diff --git a/apps/backend/src/shortener/dto/shortener.dto.ts b/apps/backend/src/shortener/dto/shortener.dto.ts
index 13cc376c..8384d559 100644
--- a/apps/backend/src/shortener/dto/shortener.dto.ts
+++ b/apps/backend/src/shortener/dto/shortener.dto.ts
@@ -9,6 +9,10 @@ export class ShortenerDto {
)
url: string;
+ @IsString()
+ @IsOptional()
+ urlKey?: string;
+
@IsString()
@IsOptional()
description?: string;
diff --git a/apps/backend/src/shortener/shortener.service.ts b/apps/backend/src/shortener/shortener.service.ts
index f2b654ef..531d1575 100644
--- a/apps/backend/src/shortener/shortener.service.ts
+++ b/apps/backend/src/shortener/shortener.service.ts
@@ -81,9 +81,10 @@ export class ShortenerService {
* Create a short URL based on the provided data.
* @param {string} url - The original URL.
* @param {number} ttl The time to live.
+ * @param {number} urlKey The desired url key.
* @returns {Promise<{ key: string }>} Returns an object containing the newly created key.
*/
- createShortenedUrl = async (url: string, ttl?: number): Promise<{ key: string }> => {
+ createShortenedUrl = async (url: string, ttl?: number, urlKey = ''): Promise<{ key: string }> => {
let parsedUrl: URL;
try {
parsedUrl = new URL(url);
@@ -98,10 +99,16 @@ export class ShortenerService {
let shortUrl: string;
- do {
- shortUrl = this.generateKey();
- } while (!(await this.isKeyAvailable(shortUrl)));
-
+ if (typeof urlKey === 'string' && urlKey.length > 0) {
+ if (!(await this.isKeyAvailable(urlKey))) {
+ throw new BadRequestException('The provided shortened key is already in use');
+ }
+ shortUrl = urlKey;
+ } else {
+ do {
+ shortUrl = this.generateKey();
+ } while (!(await this.isKeyAvailable(shortUrl)));
+ }
await this.addLinkToCache(parsedUrl.href, shortUrl, ttl);
return { key: shortUrl };
};
@@ -163,7 +170,7 @@ export class ShortenerService {
* @returns {Promise<{ key: string }>} - Returns an object containing the newly created short URL.
*/
createUsersShortenedUrl = async (user: UserContext, shortenerDto: ShortenerDto): Promise<{ key: string }> => {
- const { key } = await this.createShortenedUrl(shortenerDto.url, shortenerDto.ttl);
+ const { key } = await this.createShortenedUrl(shortenerDto.url, shortenerDto.ttl, shortenerDto.urlKey);
await this.createDbUrl(user, shortenerDto, key);
return { key };
diff --git a/apps/frontend/src/components/dashboard/links/link-modal/link-modal.tsx b/apps/frontend/src/components/dashboard/links/link-modal/link-modal.tsx
index ec73de38..f2c9fe0f 100644
--- a/apps/frontend/src/components/dashboard/links/link-modal/link-modal.tsx
+++ b/apps/frontend/src/components/dashboard/links/link-modal/link-modal.tsx
@@ -4,11 +4,12 @@ import { Form, globalAction$, zod$ } from '@builder.io/qwik-city';
import { z } from 'zod';
import { ACCESS_COOKIE_NAME } from '../../../../shared/auth.service';
import { normalizeUrl } from '../../../../utils';
+import { s } from 'vitest/dist/types-198fd1d9';
export const LINK_MODAL_ID = 'link-modal';
const useCreateLink = globalAction$(
- async ({ url }, { fail, cookie }) => {
+ async ({ url, urlKey }, { fail, cookie }) => {
const response: Response = await fetch(`${process.env.API_DOMAIN}/api/v1/shortener`, {
method: 'POST',
headers: {
@@ -18,6 +19,7 @@ const useCreateLink = globalAction$(
body: JSON.stringify({
url: normalizeUrl(url),
expirationTime: null, // forever
+ urlKey,
}),
});
@@ -45,6 +47,7 @@ const useCreateLink = globalAction$(
.regex(/^(?:https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(?::\d{1,5})?(?:\/\S*)?$/, {
message: "The url you've entered is not valid",
}),
+ urlKey: z.string(),
})
);
@@ -52,13 +55,15 @@ export interface LinkModalProps {
onSubmitHandler: () => void;
}
+const initValues = { url: '', urlKey: '' };
+
export const LinkModal = component$(({ onSubmitHandler }: LinkModalProps) => {
- const inputValue = useSignal('');
+ const inputValue = useSignal({ ...initValues });
const action = useCreateLink();
const clearValues = $(() => {
- inputValue.value = '';
+ inputValue.value = { ...initValues };
if (action.value?.fieldErrors) {
action.value.fieldErrors.url = [];
@@ -106,9 +111,22 @@ export const LinkModal = component$(({ onSubmitHandler }: LinkModalProps) => {
type="text"
placeholder="This should be a very long url..."
class="input input-bordered w-full"
- value={inputValue.value}
+ value={inputValue.value.url}
+ onInput$={(ev: InputEvent) => {
+ inputValue.value.url = (ev.target as HTMLInputElement).value;
+ }}
+ />
+
+ {
- inputValue.value = (ev.target as HTMLInputElement).value;
+ inputValue.value.urlKey = (ev.target as HTMLInputElement).value;
}}
/>
{action.value?.fieldErrors?.url && (