feat add company links
continuous-integration/drone/push Build is passing

This commit is contained in:
AmirReza Jamali
2026-07-11 15:34:13 +03:30
parent 37aa916ad5
commit 624614f1a9
10 changed files with 278 additions and 33 deletions
@@ -54,7 +54,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
{editMode ? "Edit company" : "New company"}
</h1>
<p className="text-sm text-dark-5 dark:text-dark-6">
Profile, business details, address, tags, and supporting documents.
Profile, business details, address, tags, documents, and links.
</p>
</header>
}
@@ -11,6 +11,7 @@ import type { ILabelValue } from "@/types/shared";
import type { UseFormSetValue, UseFormWatch } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import CompanyDocuments from "./documents/CompanyDocuments";
import CompanyLinks from "./links/CompanyLinks";
const companyInformationSectionId = uuidv4();
const businessInformationSectionId = uuidv4();
@@ -18,6 +19,7 @@ const settingsSectionId = uuidv4();
const addressSectionId = uuidv4();
const tagsSectionId = uuidv4();
const documentsSectionId = uuidv4();
const linksSectionId = uuidv4();
interface CompanyFormSectionsOptions {
editMode?: boolean;
@@ -169,9 +171,8 @@ const getTimezoneOffsetMinutes = (timezone: string, date = new Date()) => {
timeZoneName: "shortOffset",
});
const offset =
formatter
.formatToParts(date)
.find((part) => part.type === "timeZoneName")?.value ?? "";
formatter.formatToParts(date).find((part) => part.type === "timeZoneName")
?.value ?? "";
const label = offset.replace(/^GMT/, "") || "+0";
const match = label.match(/^([+-])(\d{1,2})(?::(\d{2}))?$/);
@@ -667,4 +668,19 @@ export const createCompanyFormSections = ({
},
],
},
{
id: linksSectionId,
title: "Links",
rootClassName: "lg:col-span-2",
className: "md:grid-cols-1",
fields: [
{
kind: "custom",
name: "links",
render: ({ control, errors, register }) => (
<CompanyLinks control={control} errors={errors} register={register} />
),
},
],
},
];
@@ -0,0 +1,110 @@
"use client";
import InputGroup from "@/components/FormElements/InputGroup";
import type { ICreateCompanyCredentials } from "@/lib/api";
import type { Control, FieldErrors, UseFormRegister } from "react-hook-form";
import { get, useFieldArray } from "react-hook-form";
const MAX_COMPANY_LINKS = 20;
interface CompanyLinksProps {
control: Control<ICreateCompanyCredentials>;
register: UseFormRegister<ICreateCompanyCredentials>;
errors: FieldErrors<ICreateCompanyCredentials>;
}
const validateHttpsUrl = (value?: string) => {
const trimmedValue = value?.trim() ?? "";
if (!trimmedValue) return "URL is required";
try {
const url = new URL(trimmedValue);
return url.protocol === "https:" || "Enter a valid HTTPS URL";
} catch {
return "Enter a valid HTTPS URL";
}
};
const CompanyLinks = ({ control, register, errors }: CompanyLinksProps) => {
const { fields, append, remove } = useFieldArray({
control,
name: "links",
});
return (
<div className="space-y-4">
{fields.length === 0 ? (
<p className="text-sm text-dark-5 dark:text-dark-6">
No external links added yet.
</p>
) : null}
{fields.map((field, index) => {
const urlName = `links.${index}.url` as const;
const titleName = `links.${index}.title` as const;
return (
<div
key={field.id}
className="grid gap-4 rounded-xl border border-stroke/70 bg-gray-1/50 p-4 dark:border-dark-3 dark:bg-dark-2/50 sm:grid-cols-2"
>
<InputGroup<ICreateCompanyCredentials>
{...register(titleName, {
setValueAs: (value: string) => value.trim(),
maxLength: {
value: 200,
message: "Title must be 200 characters or fewer",
},
})}
name={titleName}
label="Title (optional)"
type="text"
placeholder="LinkedIn"
errors={errors}
/>
<InputGroup<ICreateCompanyCredentials>
{...register(urlName, {
validate: validateHttpsUrl,
setValueAs: (value: string) => value.trim(),
})}
name={urlName}
label="URL"
type="url"
placeholder="https://example.com/profile"
required
errors={errors}
/>
<div className="flex items-center justify-between sm:col-span-2">
<span className="text-xs text-error">
{get(errors, `links.${index}.root`)?.message ?? ""}
</span>
<button
type="button"
onClick={() => remove(index)}
className="rounded-lg border border-error/30 px-3 py-1.5 text-sm font-medium text-error transition hover:bg-error/10"
>
Remove
</button>
</div>
</div>
);
})}
<div className="flex items-center justify-between gap-4">
<p className="text-xs text-dark-5 dark:text-dark-6">
{fields.length}/{MAX_COMPANY_LINKS} links
</p>
<button
type="button"
disabled={fields.length >= MAX_COMPANY_LINKS}
onClick={() => append({ url: "", title: "" })}
className="rounded-xl bg-primary px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
>
+ Add link
</button>
</div>
</div>
);
};
export default CompanyLinks;
@@ -30,6 +30,7 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
mode: "onChange",
defaultValues: {
...defaultValues,
links: defaultValues?.links ?? [],
},
});
@@ -72,6 +73,10 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
employee_count: optionalNumber(data.employee_count),
annual_revenue: optionalNumber(data.annual_revenue),
document_file_ids: documentFileIds ?? [],
links: (data.links ?? []).map((link) => ({
url: link.url.trim(),
...(link.title?.trim() ? { title: link.title.trim() } : {}),
})),
tags: {
keywords: data?.tags?.keywords?.length ? data?.tags?.keywords : [],
categories: data?.tags?.categories?.length ? data?.tags?.categories : [],