mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 16:22:59 +10:00
This commit is contained in:
@ -33,7 +33,8 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@reactive-resume/ui";
|
} from "@reactive-resume/ui";
|
||||||
import { cn, generateRandomName, kebabCase } from "@reactive-resume/utils";
|
import { cn, generateRandomName } from "@reactive-resume/utils";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -71,7 +72,7 @@ export const ResumeDialog = () => {
|
|||||||
}, [isOpen, payload]);
|
}, [isOpen, payload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const slug = kebabCase(form.watch("title"));
|
const slug = slugify(form.watch("title"));
|
||||||
form.setValue("slug", slug);
|
form.setValue("slug", slug);
|
||||||
}, [form.watch("title")]);
|
}, [form.watch("title")]);
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ export const ResumeDialog = () => {
|
|||||||
const onGenerateRandomName = () => {
|
const onGenerateRandomName = () => {
|
||||||
const name = generateRandomName();
|
const name = generateRandomName();
|
||||||
form.setValue("title", name);
|
form.setValue("title", name);
|
||||||
form.setValue("slug", kebabCase(name));
|
form.setValue("slug", slugify(name));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCreateSample = async () => {
|
const onCreateSample = async () => {
|
||||||
@ -130,7 +131,7 @@ export const ResumeDialog = () => {
|
|||||||
|
|
||||||
await duplicateResume({
|
await duplicateResume({
|
||||||
title: randomName,
|
title: randomName,
|
||||||
slug: kebabCase(randomName),
|
slug: slugify(randomName),
|
||||||
data: sampleResume,
|
data: sampleResume,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -209,6 +209,8 @@ export class PrinterService {
|
|||||||
|
|
||||||
return resumeUrl;
|
return resumeUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
ErrorMessage.ResumePrinterError,
|
ErrorMessage.ResumePrinterError,
|
||||||
(error as Error).message,
|
(error as Error).message,
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import { Prisma } from "@prisma/client";
|
|||||||
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
|
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
|
||||||
import { defaultResumeData, ResumeData } from "@reactive-resume/schema";
|
import { defaultResumeData, ResumeData } from "@reactive-resume/schema";
|
||||||
import type { DeepPartial } from "@reactive-resume/utils";
|
import type { DeepPartial } from "@reactive-resume/utils";
|
||||||
import { ErrorMessage, generateRandomName, kebabCase } from "@reactive-resume/utils";
|
import { ErrorMessage, generateRandomName } from "@reactive-resume/utils";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import deepmerge from "deepmerge";
|
import deepmerge from "deepmerge";
|
||||||
import { PrismaService } from "nestjs-prisma";
|
import { PrismaService } from "nestjs-prisma";
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ export class ResumeService {
|
|||||||
userId,
|
userId,
|
||||||
title: createResumeDto.title,
|
title: createResumeDto.title,
|
||||||
visibility: createResumeDto.visibility,
|
visibility: createResumeDto.visibility,
|
||||||
slug: createResumeDto.slug ?? kebabCase(createResumeDto.title),
|
slug: createResumeDto.slug ?? slugify(createResumeDto.title),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -54,7 +55,7 @@ export class ResumeService {
|
|||||||
visibility: "private",
|
visibility: "private",
|
||||||
data: importResumeDto.data,
|
data: importResumeDto.data,
|
||||||
title: importResumeDto.title ?? randomTitle,
|
title: importResumeDto.title ?? randomTitle,
|
||||||
slug: importResumeDto.slug ?? kebabCase(randomTitle),
|
slug: importResumeDto.slug ?? slugify(randomTitle),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Injectable, InternalServerErrorException, Logger, OnModuleInit } from "@nestjs/common";
|
import { Injectable, InternalServerErrorException, Logger, OnModuleInit } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { MinioClient, MinioService } from "nestjs-minio-client";
|
import { MinioClient, MinioService } from "nestjs-minio-client";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
@ -116,14 +117,19 @@ export class StorageService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
const extension = type === "resumes" ? "pdf" : "jpg";
|
const extension = type === "resumes" ? "pdf" : "jpg";
|
||||||
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
|
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
|
||||||
const filepath = `${userId}/${type}/${filename}.${extension}`;
|
|
||||||
|
let normalizedFilename = slugify(filename);
|
||||||
|
if (!normalizedFilename) normalizedFilename = createId();
|
||||||
|
|
||||||
|
const filepath = `${userId}/${type}/${normalizedFilename}.${extension}`;
|
||||||
const url = `${storageUrl}/${filepath}`;
|
const url = `${storageUrl}/${filepath}`;
|
||||||
|
|
||||||
const metadata =
|
const metadata =
|
||||||
extension === "jpg"
|
extension === "jpg"
|
||||||
? { "Content-Type": "image/jpeg" }
|
? { "Content-Type": "image/jpeg" }
|
||||||
: {
|
: {
|
||||||
"Content-Type": "application/pdf",
|
"Content-Type": "application/pdf",
|
||||||
"Content-Disposition": `attachment; filename=${filename}.${extension}`,
|
"Content-Disposition": `attachment; filename=${normalizedFilename}.${extension}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reactive-resume/utils": "*",
|
"@reactive-resume/utils": "*",
|
||||||
"@reactive-resume/schema": "*",
|
"@reactive-resume/schema": "*",
|
||||||
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"nestjs-zod": "^3.0.0",
|
"nestjs-zod": "^3.0.0",
|
||||||
"@swc/helpers": "~0.5.11",
|
"@swc/helpers": "~0.5.11",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { kebabCase } from "@reactive-resume/utils";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { createZodDto } from "nestjs-zod/dto";
|
import { createZodDto } from "nestjs-zod/dto";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const createResumeSchema = z.object({
|
export const createResumeSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
slug: z.string().min(1).transform(kebabCase).optional(),
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.transform((value) => slugify(value))
|
||||||
|
.optional(),
|
||||||
visibility: z.enum(["public", "private"]).default("private"),
|
visibility: z.enum(["public", "private"]).default("private"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { resumeDataSchema } from "@reactive-resume/schema";
|
import { resumeDataSchema } from "@reactive-resume/schema";
|
||||||
import { kebabCase } from "@reactive-resume/utils";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { createZodDto } from "nestjs-zod/dto";
|
import { createZodDto } from "nestjs-zod/dto";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const importResumeSchema = z.object({
|
export const importResumeSchema = z.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
slug: z.string().min(1).transform(kebabCase).optional(),
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.transform((value) => slugify(value))
|
||||||
|
.optional(),
|
||||||
visibility: z.enum(["public", "private"]).default("private").optional(),
|
visibility: z.enum(["public", "private"]).default("private").optional(),
|
||||||
data: resumeDataSchema,
|
data: resumeDataSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -30,17 +30,6 @@ export const extractUrl = (string: string) => {
|
|||||||
return result ? result[0] : null;
|
return result ? result[0] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const kebabCase = (string?: string | null) => {
|
|
||||||
if (!string) return "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
string
|
|
||||||
.match(/[A-Z]{2,}(?=[A-Z][a-z]+\d*|\b)|[A-Z]?[a-z]+\d*|[A-Z]|\d+/gu)
|
|
||||||
?.join("-")
|
|
||||||
.toLowerCase() ?? ""
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateRandomName = () => {
|
export const generateRandomName = () => {
|
||||||
return uniqueNamesGenerator({
|
return uniqueNamesGenerator({
|
||||||
dictionaries: [adjectives, adjectives, animals],
|
dictionaries: [adjectives, adjectives, animals],
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
getInitials,
|
getInitials,
|
||||||
isEmptyString,
|
isEmptyString,
|
||||||
isUrl,
|
isUrl,
|
||||||
kebabCase,
|
|
||||||
processUsername,
|
processUsername,
|
||||||
} from "../string";
|
} from "../string";
|
||||||
|
|
||||||
@ -40,16 +39,6 @@ describe("extractUrl", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("kebabCase", () => {
|
|
||||||
it("converts a string to kebab-case", () => {
|
|
||||||
expect(kebabCase("fooBar")).toBe("foo-bar");
|
|
||||||
expect(kebabCase("Foo Bar")).toBe("foo-bar");
|
|
||||||
expect(kebabCase("foo_bar")).toBe("foo-bar");
|
|
||||||
expect(kebabCase("")).toBe("");
|
|
||||||
expect(kebabCase(null)).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("generateRandomName", () => {
|
describe("generateRandomName", () => {
|
||||||
it("generates a random name", () => {
|
it("generates a random name", () => {
|
||||||
const name = generateRandomName();
|
const name = generateRandomName();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@reactive-resume/source",
|
"name": "@reactive-resume/source",
|
||||||
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
||||||
"version": "4.3.3",
|
"version": "4.3.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
@ -168,6 +168,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
"@radix-ui/react-visually-hidden": "^1.1.1",
|
"@radix-ui/react-visually-hidden": "^1.1.1",
|
||||||
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@swc/helpers": "^0.5.15",
|
"@swc/helpers": "^0.5.15",
|
||||||
"@tanstack/react-query": "^5.64.0",
|
"@tanstack/react-query": "^5.64.0",
|
||||||
"@tiptap/extension-highlight": "^2.11.2",
|
"@tiptap/extension-highlight": "^2.11.2",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -155,6 +155,9 @@ importers:
|
|||||||
'@radix-ui/react-visually-hidden':
|
'@radix-ui/react-visually-hidden':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@sindresorhus/slugify':
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1
|
||||||
'@swc/helpers':
|
'@swc/helpers':
|
||||||
specifier: ^0.5.15
|
specifier: ^0.5.15
|
||||||
version: 0.5.15
|
version: 0.5.15
|
||||||
@ -3749,6 +3752,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
'@sindresorhus/slugify@2.2.1':
|
||||||
|
resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@sindresorhus/transliterate@1.6.0':
|
||||||
|
resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@sinonjs/commons@3.0.1':
|
'@sinonjs/commons@3.0.1':
|
||||||
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
|
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
|
||||||
|
|
||||||
@ -15388,6 +15399,15 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/is@4.6.0': {}
|
'@sindresorhus/is@4.6.0': {}
|
||||||
|
|
||||||
|
'@sindresorhus/slugify@2.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@sindresorhus/transliterate': 1.6.0
|
||||||
|
escape-string-regexp: 5.0.0
|
||||||
|
|
||||||
|
'@sindresorhus/transliterate@1.6.0':
|
||||||
|
dependencies:
|
||||||
|
escape-string-regexp: 5.0.0
|
||||||
|
|
||||||
'@sinonjs/commons@3.0.1':
|
'@sinonjs/commons@3.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
type-detect: 4.0.8
|
type-detect: 4.0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user