-
-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implemented company_logo insert option, and salary range
- Loading branch information
Showing
8 changed files
with
2,363 additions
and
816 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,120 @@ | ||
import { Topics } from 'shared/src/enums/topics' | ||
import { z } from 'zod' | ||
|
||
export const formSchema = z.object({ | ||
url: z | ||
.string({ required_error: 'Campo obrigatório.' }) | ||
.url({ message: 'URL inválida.' }), | ||
title: z.string({ required_error: 'O título da vaga é obrigatório.' }), | ||
company: z.string({ required_error: 'Sem empresa -> Sem vaga 😶🌫️' }), | ||
currency: z.string({ required_error: 'Moeda inválida.' }), | ||
description: z.string({ required_error: 'Campo obrigatório.' }).nullable(), | ||
language: z.string({ required_error: 'Idioma inválido.' }), | ||
skillsId: z.array(z.string(), { | ||
required_error: 'Adicione ao menos uma habilidade.', | ||
}), | ||
country: z.string({ required_error: 'País de origem inválido.' }), | ||
minimumYears: z.number({ coerce: true }).default(0).nullable(), | ||
topicsId: z | ||
.string({ invalid_type_error: 'Selecione algum tópico.' }) | ||
.default(Topics.NATIONAL_VACANCIES.toString()), | ||
salary: z | ||
.number({ required_error: 'Salário inválido.', coerce: true }) | ||
.default(0) | ||
.nullable(), | ||
}) | ||
const formatSalary = ( | ||
minSalary: number, | ||
maxSalary: number, | ||
currency: string, | ||
frequency: 'monthly' | 'annual', | ||
isSingleValue: boolean, | ||
topicId: number | ||
): string => { | ||
const isInternational = topicId === Topics.INTERNATIONAL_VACANCIES | ||
const currencySymbol = currency === 'USD' ? '$' : 'R$' | ||
const formatter = new Intl.NumberFormat( | ||
currency === 'USD' ? 'en-US' : 'pt-BR', | ||
{ | ||
style: 'currency', | ||
currency: currency === 'USD' ? 'USD' : 'BRL', | ||
minimumFractionDigits: 2, | ||
maximumFractionDigits: 2, | ||
} | ||
) | ||
|
||
export type FormSchema = z.TypeOf<typeof formSchema> | ||
const formattedMinSalary = formatter | ||
.format(minSalary) | ||
.replace(currencySymbol, '') | ||
.trim() | ||
const formattedMaxSalary = formatter | ||
.format(maxSalary) | ||
.replace(currencySymbol, '') | ||
.trim() | ||
|
||
const frequencyText = isInternational | ||
? frequency === 'monthly' | ||
? 'monthly' | ||
: 'annual' | ||
: frequency === 'monthly' | ||
? 'mensal' | ||
: 'anual' | ||
|
||
if (isSingleValue) { | ||
return isInternational | ||
? `${currencySymbol} ${formattedMinSalary} /${frequencyText}` | ||
: `${currencySymbol} ${formattedMinSalary} /${frequencyText}` | ||
} else { | ||
return isInternational | ||
? `From ${currencySymbol} ${formattedMinSalary} to ${currencySymbol} ${formattedMaxSalary} /${frequencyText}` | ||
: `De ${currencySymbol} ${formattedMinSalary} até ${currencySymbol} ${formattedMaxSalary} /${frequencyText}` | ||
} | ||
} | ||
|
||
export const formSchema = z | ||
.object({ | ||
url: z | ||
.string({ required_error: 'Campo obrigatório.' }) | ||
.url({ message: 'URL inválida.' }), | ||
title: z.string({ required_error: 'O título da vaga é obrigatório.' }), | ||
company: z.string({ required_error: 'Sem empresa -> Sem vaga 😶🌫️' }), | ||
currency: z.enum(['USD', 'BRL'], { required_error: 'Moeda inválida.' }), | ||
description: z.string({ required_error: 'Campo obrigatório.' }).nullable(), | ||
language: z.string({ required_error: 'Idioma inválido.' }), | ||
skillsId: z.array(z.string(), { | ||
required_error: 'Adicione ao menos uma habilidade.', | ||
}), | ||
country: z.string({ required_error: 'País de origem inválido.' }), | ||
minimumYears: z.number({ coerce: true }).default(0).nullable(), | ||
topicsId: z | ||
.string({ invalid_type_error: 'Selecione algum tópico.' }) | ||
.default(Topics.NATIONAL_VACANCIES.toString()) | ||
.transform((val) => parseInt(val, 10)), | ||
minSalary: z.number().default(10), | ||
maxSalary: z.number().default(10000), | ||
salaryFrequency: z.enum(['monthly', 'annual'], { | ||
required_error: 'Por favor, selecione a frequência salarial', | ||
}), | ||
isSingleValue: z.boolean().default(false), | ||
companyLogo: z | ||
.any() | ||
.optional() | ||
.refine( | ||
(file) => { | ||
if (file instanceof File) { | ||
return ['image/jpeg', 'image/png', 'image/gif'].includes(file.type) | ||
} | ||
return true | ||
}, | ||
{ | ||
message: 'O arquivo deve ser uma imagem (JPEG, PNG ou GIF).', | ||
} | ||
) | ||
.refine( | ||
(file) => { | ||
if (file instanceof File) { | ||
return file.size <= 5 * 1024 * 1024 // 5MB | ||
} | ||
return true | ||
}, | ||
{ | ||
message: 'O tamanho máximo do arquivo é 5MB.', | ||
} | ||
), | ||
}) | ||
.refine((data) => data.maxSalary >= data.minSalary, { | ||
message: 'O salário máximo deve ser maior ou igual ao salário mínimo', | ||
path: ['maxSalary'], | ||
}) | ||
.transform((data) => ({ | ||
...data, | ||
salary: formatSalary( | ||
data.minSalary, | ||
data.isSingleValue ? data.minSalary : data.maxSalary, | ||
data.currency, | ||
data.salaryFrequency, | ||
data.isSingleValue, | ||
data.topicsId | ||
), | ||
topicId: data.topicsId, | ||
})) | ||
|
||
export type FormSchema = z.infer<typeof formSchema> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
'use client' | ||
|
||
import { zodResolver } from '@hookform/resolvers/zod' | ||
import { FormSchema, formSchema } from 'app/(roles)/formSchema' | ||
import { | ||
|
@@ -19,9 +20,16 @@ import { RolePreviewSection } from './RolePreviewSection' | |
import { RoleTopic } from './RoleTopic' | ||
import { Database } from 'db' | ||
import login from 'app/utils/LoginPreferencesActions' | ||
import { checkUserHasRoles, createRole, createRoleOwner } from './action' | ||
import { | ||
checkUserHasRoles, | ||
createRole, | ||
createRoleOwner, | ||
sendCompanyLogoToR2, | ||
} from './action' | ||
import { useRouter } from 'next/navigation' | ||
import { FEATURES } from './config' | ||
import { SalaryRangeField } from 'app/components/SalaryRangeField' | ||
import { CompanyLogoUpload } from 'app/components/CompanyLogoUpload' | ||
|
||
type FormFields = { | ||
name: keyof FormSchema | ||
|
@@ -112,6 +120,10 @@ export default function RolesCreate() { | |
const form = useForm<FormSchema>({ | ||
resolver: zodResolver(formSchema), | ||
mode: 'onBlur', | ||
defaultValues: { | ||
minSalary: 1000, | ||
maxSalary: 10000, | ||
}, | ||
}) | ||
const toast = useToast() | ||
const [isLoggedIn, setIsLoggedIn] = useState(false) | ||
|
@@ -153,45 +165,48 @@ export default function RolesCreate() { | |
}) | ||
return | ||
} | ||
|
||
try { | ||
let company_logo_url = null | ||
if (formData.companyLogo && formData.companyLogo instanceof File) { | ||
const fileName = `company_logo_${Date.now()}_${ | ||
formData.companyLogo.name | ||
}` | ||
const fileBuffer = await formData.companyLogo.arrayBuffer() | ||
const base64FileBuffer = Buffer.from(fileBuffer).toString('base64') | ||
|
||
await sendCompanyLogoToR2({ | ||
fileName, | ||
fileBuffer: base64FileBuffer, | ||
contentType: formData.companyLogo.type, | ||
}) | ||
|
||
company_logo_url = `https://company-logo-trampar-de-casa.r2.dev/${fileName}` | ||
} | ||
|
||
const roleData: RolesInsert = { | ||
language: formData.language === 'Português' ? 'Portuguese' : 'English', | ||
country: formData.country, | ||
currency: formData.currency, | ||
description: formData.description, | ||
salary: formData.salary?.toString(), | ||
salary: formData.salary, | ||
title: formData.title, | ||
url: formData.url, | ||
createdAt: new Date().toISOString(), | ||
updatedAt: new Date().toISOString(), | ||
company: formData.company, | ||
skillsId: formData.skillsId || [], | ||
ready: true, | ||
topicId: | ||
formData.currency === 'Real' || formData.currency === 'BRL' ? 1 : 2, | ||
company_logo: null, | ||
topicId: formData.topicId, | ||
company_logo: company_logo_url, | ||
minimumYears: formData.minimumYears, | ||
} | ||
|
||
const newRole = await createRole(roleData, '[email protected]') | ||
const newRole = await createRole(roleData, email) | ||
await createRoleOwner(newRole.id, userID) | ||
console.log('should redirect') | ||
router.push(`/vaga/${newRole.id}`) | ||
|
||
form.reset({ | ||
url: '', | ||
company: '', | ||
country: '', | ||
currency: undefined, | ||
description: '', | ||
language: undefined, | ||
minimumYears: undefined, | ||
salary: undefined, | ||
skillsId: undefined, | ||
title: '', | ||
topicsId: undefined, | ||
}) | ||
form.reset() | ||
|
||
toast.toast({ | ||
title: 'Vaga criada com sucesso!', | ||
|
@@ -217,34 +232,35 @@ export default function RolesCreate() { | |
</h1> | ||
<section> | ||
<RolePreviewSection /> | ||
<section className="grid grid-cols-1 gap-6 py-6 md:grid-cols-2"> | ||
<section className="grid grid-cols-1 justify-center gap-6 py-6 md:grid-cols-2"> | ||
<CompanyLogoUpload /> | ||
{fields.map((props) => ( | ||
<CustomFormField | ||
key={props.name} | ||
{...props} | ||
Input={props.Input || TextInput} | ||
/> | ||
))} | ||
<section className="grid grid-cols-2 gap-6"> | ||
{salaryAndCurrencyField.map((props) => ( | ||
<CustomFormField | ||
key={props.name} | ||
{...props} | ||
Input={props.Input || TextInput} | ||
/> | ||
))} | ||
</section> | ||
<section className="grid grid-cols-2 gap-6"> | ||
{countryAndLanguageField.map((props) => ( | ||
<CustomFormField | ||
key={props.name} | ||
{...props} | ||
Input={props.Input || TextInput} | ||
/> | ||
))} | ||
</section> | ||
<RoleTopic /> | ||
<SkillsField description="Quais habilidades são necessárias para a vaga?" /> | ||
<CustomFormField | ||
name="currency" | ||
label="Câmbio" | ||
placeholder="BRL, USD, EUR..." | ||
description="Insira a moeda de pagamento do salário" | ||
Input={CurrencySelect} | ||
required | ||
/> | ||
<SalaryRangeField currency={form.watch('currency')} /> | ||
{countryAndLanguageField.map((props) => ( | ||
<CustomFormField | ||
key={props.name} | ||
{...props} | ||
Input={props.Input || TextInput} | ||
/> | ||
))} | ||
<div className="space-y-6 md:col-span-2"> | ||
<RoleTopic /> | ||
<SkillsField description="Quais habilidades são necessárias para a vaga?" /> | ||
</div> | ||
</section> | ||
</section> | ||
<section className="flex gap-4"> | ||
|
Oops, something went wrong.