mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-21 07:23:28 +10:00
release: v4.1.0
This commit is contained in:
@@ -10,11 +10,9 @@ export const loginSchema = z
|
||||
})
|
||||
.refine(
|
||||
(value) => {
|
||||
if (value.identifier.includes("@")) {
|
||||
return z.string().email().parse(value.identifier);
|
||||
} else {
|
||||
return usernameSchema.parse(value.identifier);
|
||||
}
|
||||
return value.identifier.includes("@")
|
||||
? z.string().email().parse(value.identifier)
|
||||
: usernameSchema.parse(value.identifier);
|
||||
},
|
||||
{ message: "InvalidCredentials" },
|
||||
);
|
||||
|
||||
@@ -2,10 +2,7 @@ import { createZodDto } from "nestjs-zod/dto";
|
||||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const twoFactorSchema = z.object({
|
||||
code: z
|
||||
.string()
|
||||
.length(6)
|
||||
.regex(/^[0-9]+$/, { message: "code must be a 6 digit number" }),
|
||||
code: z.string().length(6).regex(/^\d+$/, { message: "code must be a 6 digit number" }),
|
||||
});
|
||||
|
||||
export class TwoFactorDto extends createZodDto(twoFactorSchema) {}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "nestjs-zod/z";
|
||||
|
||||
export const createResumeSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
slug: z.string().min(1).transform(kebabCase),
|
||||
slug: z.string().min(1).transform(kebabCase).optional(),
|
||||
visibility: z.enum(["public", "private"]).default("private"),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { idSchema } from "@reactive-resume/schema";
|
||||
import { createZodDto } from "nestjs-zod/dto";
|
||||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const secretsSchema = z.object({
|
||||
@@ -13,5 +12,3 @@ export const secretsSchema = z.object({
|
||||
resetToken: z.string().nullable(),
|
||||
userId: idSchema,
|
||||
});
|
||||
|
||||
export class SecretsDto extends createZodDto(secretsSchema) {}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const usernameSchema = z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(255)
|
||||
.regex(/^[a-z0-9._-]+$/, {
|
||||
.regex(/^[\d._a-z-]+$/, {
|
||||
message:
|
||||
"Usernames can only contain lowercase letters, numbers, periods, hyphens, and underscores.",
|
||||
});
|
||||
|
||||
@@ -4,7 +4,19 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
// react
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/jsx-sort-props": [
|
||||
"error",
|
||||
{
|
||||
"reservedFirst": true,
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { FieldPath, FieldValues } from "react-hook-form";
|
||||
import { FieldPath, FieldValues, useFormContext } from "react-hook-form";
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
@@ -14,16 +13,16 @@ type FormItemContextValue = { id: string };
|
||||
export const FormItemContext = createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||
|
||||
export const useFormField = () => {
|
||||
const fieldContext = useContext(FormFieldContext);
|
||||
const itemContext = useContext(FormItemContext);
|
||||
const fieldContext = useContext(FormFieldContext) as FormFieldContextValue | undefined;
|
||||
const itemContext = useContext(FormItemContext) as FormItemContextValue | undefined;
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
if (!fieldContext || !itemContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,12 +5,12 @@ const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)";
|
||||
|
||||
type Theme = "system" | "dark" | "light";
|
||||
|
||||
interface UseThemeOutput {
|
||||
type UseThemeOutput = {
|
||||
theme: Theme;
|
||||
isDarkMode: boolean;
|
||||
toggleTheme: () => void;
|
||||
setTheme: Dispatch<SetStateAction<Theme>>;
|
||||
}
|
||||
};
|
||||
|
||||
export const useTheme = (): UseThemeOutput => {
|
||||
const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY);
|
||||
@@ -23,15 +23,18 @@ export const useTheme = (): UseThemeOutput => {
|
||||
|
||||
useEffect(() => {
|
||||
switch (theme) {
|
||||
case "light":
|
||||
case "light": {
|
||||
setDarkMode(false);
|
||||
break;
|
||||
case "system":
|
||||
}
|
||||
case "system": {
|
||||
setDarkMode(isDarkOS);
|
||||
break;
|
||||
case "dark":
|
||||
}
|
||||
case "dark": {
|
||||
setDarkMode(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [theme, isDarkOS]);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import * as path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
@@ -14,7 +15,7 @@ export default defineConfig({
|
||||
nxViteTsPaths(),
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: path.join(__dirname, "tsconfig.lib.json"),
|
||||
tsconfigPath: path.join(import.meta.dirname, "tsconfig.lib.json"),
|
||||
}),
|
||||
],
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ResumeData } from "@reactive-resume/schema";
|
||||
import { ZodDto } from "nestjs-zod/dto";
|
||||
import { Schema } from "zod";
|
||||
|
||||
export interface Parser<Data = unknown, T = ZodDto, Result = ResumeData> {
|
||||
export type Parser<Data = unknown, T = ZodDto, Result = ResumeData> = {
|
||||
schema?: Schema;
|
||||
|
||||
readFile(file: File): Promise<Data>;
|
||||
@@ -10,4 +10,4 @@ export interface Parser<Data = unknown, T = ZodDto, Result = ResumeData> {
|
||||
validate(data: Data): T | Promise<T>;
|
||||
|
||||
convert(data: T): Result | Promise<Result>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,19 +33,22 @@ export class JsonResumeParser implements Parser<Json, JsonResume> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const result = JSON.parse(reader.result as string) as Json;
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
reject(new Error("Failed to parse JSON"));
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read the file"));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ const urlSchema = z.literal("").or(z.string().url()).optional();
|
||||
|
||||
const iso8601 = z
|
||||
.string()
|
||||
.regex(
|
||||
/^([1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]|[1-2][0-9]{3}-[0-1][0-9]|[1-2][0-9]{3})$/,
|
||||
"ISO8601 Date Format",
|
||||
);
|
||||
.regex(/^([12]\d{3}-[01]\d-[0-3]\d|[12]\d{3}-[01]\d|[12]\d{3})$/, "ISO8601 Date Format");
|
||||
|
||||
const locationSchema = z.object({
|
||||
address: z.string().optional(),
|
||||
|
||||
@@ -20,6 +20,11 @@ import { LinkedIn, linkedInSchema } from "./schema";
|
||||
|
||||
export * from "./schema";
|
||||
|
||||
const avoidTooShort = (name: string, len: number) => {
|
||||
if (!name || name.length < len) return "Unknown";
|
||||
return name;
|
||||
};
|
||||
|
||||
export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
schema: Schema;
|
||||
|
||||
@@ -44,8 +49,7 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
for (const key of Object.keys(linkedInSchema.shape)) {
|
||||
if (name.includes(key)) {
|
||||
const content = await file.async("text");
|
||||
const jsonArray = await parseCSV(content);
|
||||
result[key] = jsonArray;
|
||||
result[key] = await parseCSV(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,11 +60,6 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
convert(data: LinkedIn) {
|
||||
const result = JSON.parse(JSON.stringify(defaultResumeData)) as ResumeData;
|
||||
|
||||
const avoidTooShort = (name: string, len: number) => {
|
||||
if (!name || name.length < len) return "Unknown";
|
||||
return name;
|
||||
};
|
||||
|
||||
// Profile
|
||||
if (data.Profile && data.Profile.length > 0) {
|
||||
const profile = data.Profile[0];
|
||||
@@ -94,8 +93,8 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
}
|
||||
|
||||
// Positions
|
||||
if (data["Positions"] && data["Positions"].length > 0) {
|
||||
for (const position of data["Positions"]) {
|
||||
if (data.Positions && data.Positions.length > 0) {
|
||||
for (const position of data.Positions) {
|
||||
result.sections.experience.items.push({
|
||||
...defaultExperience,
|
||||
id: createId(),
|
||||
@@ -109,8 +108,8 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
}
|
||||
|
||||
// Education
|
||||
if (data["Education"] && data["Education"].length > 0) {
|
||||
for (const education of data["Education"]) {
|
||||
if (data.Education && data.Education.length > 0) {
|
||||
for (const education of data.Education) {
|
||||
result.sections.education.items.push({
|
||||
...defaultEducation,
|
||||
id: createId(),
|
||||
@@ -123,8 +122,8 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (data["Skills"] && data["Skills"].length > 0) {
|
||||
for (const skill of data["Skills"]) {
|
||||
if (data.Skills && data.Skills.length > 0) {
|
||||
for (const skill of data.Skills) {
|
||||
result.sections.skills.items.push({
|
||||
...defaultSkill,
|
||||
id: createId(),
|
||||
@@ -134,8 +133,8 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
}
|
||||
|
||||
// Languages
|
||||
if (data["Languages"] && data["Languages"].length > 0) {
|
||||
for (const language of data["Languages"]) {
|
||||
if (data.Languages && data.Languages.length > 0) {
|
||||
for (const language of data.Languages) {
|
||||
result.sections.languages.items.push({
|
||||
...defaultLanguage,
|
||||
id: createId(),
|
||||
@@ -146,8 +145,8 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
}
|
||||
|
||||
// Certifications
|
||||
if (data["Certifications"] && data["Certifications"].length > 0) {
|
||||
for (const certification of data["Certifications"]) {
|
||||
if (data.Certifications && data.Certifications.length > 0) {
|
||||
for (const certification of data.Certifications) {
|
||||
result.sections.certifications.items.push({
|
||||
...defaultCertification,
|
||||
id: createId(),
|
||||
@@ -160,8 +159,8 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
|
||||
}
|
||||
|
||||
// Projects
|
||||
if (data["Projects"] && data["Projects"].length > 0) {
|
||||
for (const project of data["Projects"]) {
|
||||
if (data.Projects && data.Projects.length > 0) {
|
||||
for (const project of data.Projects) {
|
||||
result.sections.projects.items.push({
|
||||
...defaultProject,
|
||||
id: createId(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
export const educationSchema = z.object({
|
||||
"School Name": z.string(),
|
||||
"Start Date": z.string(),
|
||||
"End Date": z.string(),
|
||||
"End Date": z.string().optional(),
|
||||
Notes: z.string().optional(),
|
||||
"Degree Name": z.string(),
|
||||
Activities: z.string(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import {
|
||||
defaultAward,
|
||||
@@ -34,19 +35,22 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const result = JSON.parse(reader.result as string) as Json;
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
reject(new Error("Failed to parse JSON"));
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read the file"));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
@@ -70,7 +74,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
result.basics.picture.url = isUrl(data.basics.photo.url) ? data.basics.photo.url! : "";
|
||||
|
||||
// Profiles
|
||||
if (data.basics.profiles) {
|
||||
if (data.basics.profiles && data.basics.profiles.length > 0) {
|
||||
for (const profile of data.basics.profiles) {
|
||||
result.sections.profiles.items.push({
|
||||
...defaultProfile,
|
||||
@@ -84,7 +88,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Work
|
||||
if (data.sections.work.items) {
|
||||
if (data.sections.work?.items && data.sections.work.items.length > 0) {
|
||||
for (const work of data.sections.work.items) {
|
||||
if (!work) continue;
|
||||
|
||||
@@ -101,7 +105,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Awards
|
||||
if (data.sections.awards.items) {
|
||||
if (data.sections.awards?.items && data.sections.awards.items.length > 0) {
|
||||
for (const award of data.sections.awards.items) {
|
||||
if (!award) continue;
|
||||
|
||||
@@ -118,7 +122,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (data.sections.skills.items) {
|
||||
if (data.sections.skills?.items && data.sections.skills.items.length > 0) {
|
||||
for (const skill of data.sections.skills.items) {
|
||||
if (!skill) continue;
|
||||
|
||||
@@ -136,7 +140,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Projects
|
||||
if (data.sections.projects.items) {
|
||||
if (data.sections.projects?.items && data.sections.projects.items.length > 0) {
|
||||
for (const project of data.sections.projects.items) {
|
||||
if (!project) continue;
|
||||
|
||||
@@ -156,7 +160,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Education
|
||||
if (data.sections.education.items) {
|
||||
if (data.sections.education?.items && data.sections.education.items.length > 0) {
|
||||
for (const education of data.sections.education.items) {
|
||||
if (!education) continue;
|
||||
|
||||
@@ -175,7 +179,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Interests
|
||||
if (data.sections.interests.items) {
|
||||
if (data.sections.interests?.items && data.sections.interests.items.length > 0) {
|
||||
for (const interest of data.sections.interests.items) {
|
||||
if (!interest) continue;
|
||||
|
||||
@@ -191,7 +195,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Languages
|
||||
if (data.sections.languages.items) {
|
||||
if (data.sections.languages?.items && data.sections.languages.items.length > 0) {
|
||||
for (const language of data.sections.languages.items) {
|
||||
if (!language) continue;
|
||||
|
||||
@@ -206,7 +210,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Volunteer
|
||||
if (data.sections.volunteer.items) {
|
||||
if (data.sections.volunteer?.items && data.sections.volunteer.items.length > 0) {
|
||||
for (const volunteer of data.sections.volunteer.items) {
|
||||
if (!volunteer) continue;
|
||||
|
||||
@@ -223,7 +227,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// References
|
||||
if (data.sections.references.items) {
|
||||
if (data.sections.references?.items && data.sections.references.items.length > 0) {
|
||||
for (const reference of data.sections.references.items) {
|
||||
if (!reference) continue;
|
||||
|
||||
@@ -238,7 +242,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Publications
|
||||
if (data.sections.publications.items) {
|
||||
if (data.sections.publications?.items && data.sections.publications.items.length > 0) {
|
||||
for (const publication of data.sections.publications.items) {
|
||||
if (!publication) continue;
|
||||
|
||||
@@ -254,7 +258,7 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
|
||||
}
|
||||
|
||||
// Certifications
|
||||
if (data.sections.certifications.items) {
|
||||
if (data.sections.certifications?.items && data.sections.certifications.items.length > 0) {
|
||||
for (const certification of data.sections.certifications.items) {
|
||||
if (!certification) continue;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const basicsSchema = z.object({
|
||||
.optional(),
|
||||
birthdate: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
profiles: z.array(profileSchema),
|
||||
profiles: z.array(profileSchema).optional(),
|
||||
location: z.object({
|
||||
address: z.string().optional(),
|
||||
postalCode: z.string().optional(),
|
||||
@@ -206,19 +206,21 @@ export const reactiveResumeV3Schema = z.object({
|
||||
public: z.boolean(),
|
||||
basics: basicsSchema,
|
||||
sections: z.object({
|
||||
work: sectionSchema.extend({ items: z.array(workSchema) }),
|
||||
awards: sectionSchema.extend({ items: z.array(awardSchema) }),
|
||||
skills: sectionSchema.extend({ items: z.array(skillSchema) }),
|
||||
projects: sectionSchema.extend({ items: z.array(projectSchema) }),
|
||||
education: sectionSchema.extend({ items: z.array(educationSchema) }),
|
||||
interests: sectionSchema.extend({ items: z.array(interestSchema) }),
|
||||
languages: sectionSchema.extend({ items: z.array(languageSchema) }),
|
||||
volunteer: sectionSchema.extend({ items: z.array(volunteerSchema) }),
|
||||
references: sectionSchema.extend({ items: z.array(referenceSchema) }),
|
||||
publications: sectionSchema.extend({ items: z.array(publicationSchema) }),
|
||||
certifications: sectionSchema.extend({
|
||||
items: z.array(certificationSchema),
|
||||
}),
|
||||
work: sectionSchema.extend({ items: z.array(workSchema) }).optional(),
|
||||
awards: sectionSchema.extend({ items: z.array(awardSchema) }).optional(),
|
||||
skills: sectionSchema.extend({ items: z.array(skillSchema) }).optional(),
|
||||
projects: sectionSchema.extend({ items: z.array(projectSchema) }).optional(),
|
||||
education: sectionSchema.extend({ items: z.array(educationSchema) }).optional(),
|
||||
interests: sectionSchema.extend({ items: z.array(interestSchema) }).optional(),
|
||||
languages: sectionSchema.extend({ items: z.array(languageSchema) }).optional(),
|
||||
volunteer: sectionSchema.extend({ items: z.array(volunteerSchema) }).optional(),
|
||||
references: sectionSchema.extend({ items: z.array(referenceSchema) }).optional(),
|
||||
publications: sectionSchema.extend({ items: z.array(publicationSchema) }).optional(),
|
||||
certifications: sectionSchema
|
||||
.extend({
|
||||
items: z.array(certificationSchema),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
metadata: metadataSchema,
|
||||
});
|
||||
|
||||
@@ -15,19 +15,22 @@ export class ReactiveResumeParser implements Parser<Json, ResumeData> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const result = JSON.parse(reader.result as string) as Json;
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
reject(new Error("Failed to parse JSON"));
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read the file"));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,4 @@ export const customFieldSchema = z.object({
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const customFieldsDefault = [];
|
||||
|
||||
export type CustomField = z.infer<typeof customFieldSchema>;
|
||||
|
||||
+13
-1
@@ -11,7 +11,19 @@
|
||||
"config": "tailwind.config.js"
|
||||
}
|
||||
},
|
||||
"rules": {}
|
||||
"rules": {
|
||||
// react
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/jsx-sort-props": [
|
||||
"error",
|
||||
{
|
||||
"reservedFirst": true,
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const { join } = require("path");
|
||||
const path = require("node:path");
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: { config: join(__dirname, "tailwind.config.js") },
|
||||
tailwindcss: { config: path.join(__dirname, "tailwind.config.js") },
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ export const AlertDialogContent = forwardRef<
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded border bg-background p-6 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] md:w-full",
|
||||
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded border bg-background p-6 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] md:w-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { forwardRef } from "react";
|
||||
|
||||
import { alertVariants } from "../variants/alert";
|
||||
|
||||
interface AlertProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof alertVariants> {}
|
||||
type AlertProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>;
|
||||
|
||||
export const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
({ className, variant, ...props }, ref) => (
|
||||
|
||||
@@ -47,7 +47,9 @@ export const BadgeInput = forwardRef<HTMLInputElement, BadgeInputProps>(
|
||||
ref={ref}
|
||||
value={label}
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={(event) => setLabel(event.target.value)}
|
||||
onChange={(event) => {
|
||||
setLabel(event.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -3,9 +3,7 @@ import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { badgeVariants } from "../variants/badge";
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
export type BadgeProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>;
|
||||
|
||||
export const Badge = ({ className, variant, outline, ...props }: BadgeProps) => (
|
||||
<div className={cn(badgeVariants({ variant, outline }), className)} {...props} />
|
||||
|
||||
@@ -5,11 +5,10 @@ import { forwardRef } from "react";
|
||||
|
||||
import { buttonVariants } from "../variants/button";
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
export type ButtonProps = {
|
||||
asChild?: boolean;
|
||||
}
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
} from "./command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
||||
|
||||
export interface ComboboxOption {
|
||||
export type ComboboxOption = {
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
type ComboboxPropsSingle = {
|
||||
options: ComboboxOption[];
|
||||
|
||||
@@ -38,7 +38,7 @@ export const DialogContent = forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-sm translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 duration-200 focus:outline-none focus:ring-1 focus:ring-secondary focus:ring-offset-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:max-w-xl sm:rounded-sm md:w-full",
|
||||
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-sm -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 duration-200 focus:outline-none focus:ring-1 focus:ring-secondary focus:ring-offset-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:max-w-xl sm:rounded-sm md:w-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const DropdownMenuSubContent = forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
@@ -50,7 +50,7 @@ export const DropdownMenuSubContent = forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPortal>
|
||||
));
|
||||
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
@@ -3,12 +3,10 @@ import { Slot } from "@radix-ui/react-slot";
|
||||
import { FormFieldContext, FormItemContext, useFormField } from "@reactive-resume/hooks";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { forwardRef, useId } from "react";
|
||||
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider } from "react-hook-form";
|
||||
import { Controller, ControllerProps, FieldPath, FieldValues } from "react-hook-form";
|
||||
|
||||
import { Label } from "./label";
|
||||
|
||||
export const Form = FormProvider;
|
||||
|
||||
export const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
@@ -63,7 +61,7 @@ export const FormControl = forwardRef<
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||
aria-describedby={error ? `${formDescriptionId} ${formMessageId}` : formDescriptionId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -94,7 +92,7 @@ export const FormMessage = forwardRef<
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
const body = error ? String(error.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
@@ -113,3 +111,5 @@ export const FormMessage = forwardRef<
|
||||
});
|
||||
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export { FormProvider as Form } from "react-hook-form";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
export type InputProps = {
|
||||
hasError?: boolean;
|
||||
}
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, hasError = false, ...props }, ref) => (
|
||||
|
||||
@@ -85,7 +85,7 @@ const InsertImageForm = ({ onInsert }: InsertImageProps) => {
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
|
||||
<form className="space-y-3" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<p className="prose prose-sm prose-zinc dark:prose-invert">
|
||||
Insert an image from an external URL and use it on your resume.
|
||||
</p>
|
||||
@@ -97,7 +97,7 @@ const InsertImageForm = ({ onInsert }: InsertImageProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="http://..." {...field} />
|
||||
<Input placeholder="https://..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -347,8 +347,8 @@ const Toolbar = ({ editor }: { editor: Editor }) => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
onClick={() => editor.chain().focus().liftListItem("listItem").run()}
|
||||
disabled={!editor.can().chain().focus().liftListItem("listItem").run()}
|
||||
onClick={() => editor.chain().focus().liftListItem("listItem").run()}
|
||||
>
|
||||
<TextOutdent />
|
||||
</Button>
|
||||
@@ -359,8 +359,8 @@ const Toolbar = ({ editor }: { editor: Editor }) => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
onClick={() => editor.chain().focus().sinkListItem("listItem").run()}
|
||||
disabled={!editor.can().chain().focus().sinkListItem("listItem").run()}
|
||||
onClick={() => editor.chain().focus().sinkListItem("listItem").run()}
|
||||
>
|
||||
<TextIndent />
|
||||
</Button>
|
||||
@@ -384,8 +384,8 @@ const Toolbar = ({ editor }: { editor: Editor }) => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
onClick={() => editor.chain().focus().setHardBreak().run()}
|
||||
disabled={!editor.can().chain().focus().setHardBreak().run()}
|
||||
onClick={() => editor.chain().focus().setHardBreak().run()}
|
||||
>
|
||||
<KeyReturn />
|
||||
</Button>
|
||||
@@ -396,8 +396,8 @@ const Toolbar = ({ editor }: { editor: Editor }) => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
disabled={!editor.can().chain().focus().setHorizontalRule().run()}
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
@@ -430,18 +430,14 @@ const Toolbar = ({ editor }: { editor: Editor }) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface RichInputProps
|
||||
extends Omit<
|
||||
EditorContentProps,
|
||||
"ref" | "editor" | "content" | "value" | "onChange" | "className"
|
||||
> {
|
||||
type RichInputProps = {
|
||||
content?: string;
|
||||
onChange?: (value: string) => void;
|
||||
hideToolbar?: boolean;
|
||||
className?: string;
|
||||
editorClassName?: string;
|
||||
footer?: (editor: Editor) => React.ReactNode;
|
||||
}
|
||||
} & Omit<EditorContentProps, "ref" | "editor" | "content" | "value" | "onChange" | "className">;
|
||||
|
||||
export const RichInput = forwardRef<Editor, RichInputProps>(
|
||||
(
|
||||
|
||||
@@ -34,11 +34,10 @@ export const SheetOverlay = forwardRef<
|
||||
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
export interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
export type SheetContentProps = {
|
||||
showClose?: boolean;
|
||||
}
|
||||
} & React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> &
|
||||
VariantProps<typeof sheetVariants>;
|
||||
|
||||
export const SheetContent = forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
|
||||
@@ -20,8 +20,13 @@ export const KeyboardShortcut = ({
|
||||
const { value, setValue } = useBoolean(defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => e.key === "Control" && setValue(true);
|
||||
const onKeyUp = (e: KeyboardEvent) => e.key === "Control" && setValue(false);
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
e.key === "Control" && setValue(true);
|
||||
};
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
e.key === "Control" && setValue(false);
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import * as path from "path";
|
||||
import { defineConfig, searchForWorkspaceRoot } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
@@ -18,7 +19,7 @@ export default defineConfig({
|
||||
nxViteTsPaths(),
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: path.join(__dirname, "tsconfig.lib.json"),
|
||||
tsconfigPath: path.join(import.meta.dirname, "tsconfig.lib.json"),
|
||||
}),
|
||||
],
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import { LayoutLocator } from "./types";
|
||||
|
||||
// Function to find a specific item in a layout
|
||||
export const findItemInLayout = (item: string, layout: string[][][]): LayoutLocator | null => {
|
||||
for (let page = 0; page < layout.length; page++) {
|
||||
for (let column = 0; column < layout[page].length; column++) {
|
||||
for (let section = 0; section < layout[page][column].length; section++) {
|
||||
if (layout[page][column][section] === item) {
|
||||
for (const [page, element] of layout.entries()) {
|
||||
for (const [column, element_] of element.entries()) {
|
||||
for (const [section, element__] of element_.entries()) {
|
||||
if (element__ === item) {
|
||||
return { page, column, section };
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export const moveItemInLayout = (
|
||||
newLayout[target.page][target.column].splice(target.section, 0, item);
|
||||
|
||||
return newLayout;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return layout;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
export const hexToRgb = (hex: string, alpha = 0) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16),
|
||||
g = parseInt(hex.slice(3, 5), 16),
|
||||
b = parseInt(hex.slice(5, 7), 16);
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16),
|
||||
g = Number.parseInt(hex.slice(3, 5), 16),
|
||||
b = Number.parseInt(hex.slice(5, 7), 16);
|
||||
|
||||
if (alpha) {
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
} else {
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
return alpha ? `rgba(${r}, ${g}, ${b}, ${alpha})` : `rgb(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
@@ -7,16 +7,20 @@ export const parseCSV = async (string: string) => {
|
||||
Papa.parse(string, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (results) => resolve(results.data as Json[]),
|
||||
error: (error: Error) => reject(error),
|
||||
complete: (results) => {
|
||||
resolve(results.data as Json[]);
|
||||
},
|
||||
error: (error: Error) => {
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parser for cases when we receive an array like structure f.e. a in the LinkedIn Profile.csv import
|
||||
* Parser for cases when we receive an array like structure f.e. in the LinkedIn Profile.csv import
|
||||
* @param csvEntry array-like entry such as [TAG:https://some.link,TAG:https://someother.link]
|
||||
* @returns
|
||||
*/
|
||||
export const parseArrayLikeCSVEntry = (csvEntry: string) =>
|
||||
csvEntry.replace(/^\[/, "").replace(/$\]/, "").split(",");
|
||||
csvEntry.replace(/^\[/, "").replace(/$]/, "").split(",");
|
||||
|
||||
@@ -24,12 +24,10 @@ export const deepSearchAndParseDates = (obj: any, dateKeys: string[]): any => {
|
||||
for (const key of keys) {
|
||||
let value = obj[key];
|
||||
|
||||
if (dateKeys.includes(key)) {
|
||||
if (typeof value === "string") {
|
||||
const parsedDate = new Date(value);
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
value = parsedDate;
|
||||
}
|
||||
if (dateKeys.includes(key) && typeof value === "string") {
|
||||
const parsedDate = new Date(value);
|
||||
if (!Number.isNaN(parsedDate.getTime())) {
|
||||
value = parsedDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export type Font = {
|
||||
category: string;
|
||||
subsets: string[];
|
||||
variants: string[];
|
||||
files: { [key: string]: string };
|
||||
files: Record<string, string>;
|
||||
};
|
||||
|
||||
export const fonts: Font[] = [
|
||||
|
||||
@@ -5,6 +5,6 @@ export const linearTransform = (
|
||||
outMin: number,
|
||||
outMax: number,
|
||||
) => {
|
||||
if (inMax === inMin) return value === inMax ? outMin : NaN;
|
||||
if (inMax === inMin) return value === inMax ? outMin : Number.NaN;
|
||||
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
||||
};
|
||||
|
||||
@@ -3,16 +3,17 @@ import { adjectives, animals, uniqueNamesGenerator } from "unique-names-generato
|
||||
import { LayoutLocator, SortablePayload } from "./types";
|
||||
|
||||
export const getInitials = (name: string) => {
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
const regex = new RegExp(/(\p{L}{1})\p{L}+/, "gu");
|
||||
const initials = [...name.matchAll(regex)] || [];
|
||||
const initials = [...name.matchAll(regex)];
|
||||
|
||||
return ((initials.shift()?.[1] || "") + (initials.pop()?.[1] || "")).toUpperCase();
|
||||
return ((initials.shift()?.[1] ?? "") + (initials.pop()?.[1] ?? "")).toUpperCase();
|
||||
};
|
||||
|
||||
export const isUrl = (string: string | null | undefined) => {
|
||||
if (!string) return false;
|
||||
|
||||
const urlRegex = /https?:\/\/[^ \n]+/i;
|
||||
const urlRegex = /https?:\/\/[^\n ]+/i;
|
||||
|
||||
return urlRegex.test(string);
|
||||
};
|
||||
@@ -23,7 +24,7 @@ export const isEmptyString = (string: string) => {
|
||||
};
|
||||
|
||||
export const extractUrl = (string: string) => {
|
||||
const urlRegex = /https?:\/\/[^ \n]+/i;
|
||||
const urlRegex = /https?:\/\/[^\n ]+/i;
|
||||
|
||||
const result = string.match(urlRegex);
|
||||
return result ? result[0] : null;
|
||||
@@ -34,7 +35,7 @@ export const kebabCase = (string?: string | null) => {
|
||||
|
||||
return (
|
||||
string
|
||||
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
||||
.match(/[A-Z]{2,}(?=[A-Z][a-z]+\d*|\b)|[A-Z]?[a-z]+\d*|[A-Z]|\d+/g)
|
||||
?.join("-")
|
||||
.toLowerCase() ?? ""
|
||||
);
|
||||
@@ -52,13 +53,13 @@ export const generateRandomName = () => {
|
||||
export const processUsername = (string?: string | null) => {
|
||||
if (!string) return "";
|
||||
|
||||
return string.replace(/[^a-zA-Z0-9-.]/g, "").toLowerCase();
|
||||
return string.replace(/[^\d.A-Za-z-]/g, "").toLowerCase();
|
||||
};
|
||||
|
||||
export const parseLayoutLocator = (payload: SortablePayload | null): LayoutLocator => {
|
||||
if (!payload) return { page: 0, column: 0, section: 0 };
|
||||
|
||||
const section = payload.index as number;
|
||||
const section = payload.index;
|
||||
const [page, column] = payload.containerId.split(".").map(Number);
|
||||
|
||||
return { page, column, section };
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("linearTransform", () => {
|
||||
it("returns NaN if input maximum equals input minimum", () => {
|
||||
const value = 5;
|
||||
const result = linearTransform(value, 0, 0, 0, 100);
|
||||
expect(result).toBe(NaN);
|
||||
expect(result).toBe(Number.NaN);
|
||||
});
|
||||
|
||||
it("returns NaN if input range is zero (avoids division by zero)", () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("exclude", () => {
|
||||
age: 30,
|
||||
email: "alice@example.com",
|
||||
};
|
||||
const keysToExclude: Array<keyof TestObject> = [];
|
||||
const keysToExclude: (keyof TestObject)[] = [];
|
||||
const result = exclude(object, keysToExclude);
|
||||
|
||||
expect(result).toEqual(object);
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("kebabCase", () => {
|
||||
describe("generateRandomName", () => {
|
||||
it("generates a random name", () => {
|
||||
const name = generateRandomName();
|
||||
expect(name).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$/);
|
||||
expect(name).toMatch(/^(?:[A-Z][a-z]+ ){2}[A-Z][a-z]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user