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 && (