Skip to content

Commit

Permalink
feat: edit links (#33)
Browse files Browse the repository at this point in the history
* chore: create types and schemas for the link edit

* feat: functions for the editing of the link with the id

* chore: create the visual for the editing of the linsk

* chore: format code
  • Loading branch information
Michael-Liendo authored Aug 18, 2024
1 parent 095ce08 commit bd41465
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 24 deletions.
66 changes: 46 additions & 20 deletions client/src/components/datagrids/links/linkx-table-row-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
import type { Row } from '@tanstack/react-table';

import { TextField } from '@/components/text-field';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '../../ui/button';
import {
DropdownMenu,
Expand All @@ -13,7 +23,11 @@ import {
} from '../../ui/dropdown-menu';

import useLinks from '@/hooks/useLinks';
import { LinkSchema } from '@linx/shared';
import { toFormikValidationSchema } from '@/utils/toFormikValidationSchema';
import { LinkForUpdateSchema, LinkSchema } from '@linx/shared';
import { useFormik } from 'formik';
import { useState } from 'react';
import { LinkDialogEdit } from './modal-edit';

interface DataTableRowActionsProps<TData> {
row: Row<TData>;
Expand All @@ -22,27 +36,39 @@ interface DataTableRowActionsProps<TData> {
export function DataTableRowActions<TData>({
row,
}: DataTableRowActionsProps<TData>) {
const { deleteById } = useLinks();
const link = LinkSchema.parse(row.original);

const [isOpen, setIsOpen] = useState(false);

const { deleteById } = useLinks();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => deleteById(link.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DialogTrigger asChild>
<DropdownMenuItem>Edit</DropdownMenuItem>
</DialogTrigger>

<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => deleteById(link.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* dialog */}
<LinkDialogEdit link={link} setIsOpen={setIsOpen} />
</Dialog>
</>
);
}
80 changes: 80 additions & 0 deletions client/src/components/datagrids/links/modal-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { TextField } from '@/components/text-field';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import useLinks from '@/hooks/useLinks';
import { toFormikValidationSchema } from '@/utils/toFormikValidationSchema';
import { type ILink, LinkForUpdateSchema } from '@linx/shared';
import { Link1Icon } from '@radix-ui/react-icons';
import { useFormik } from 'formik';
import { useState } from 'react';

export function LinkDialogEdit({
link,
setIsOpen,
}: { link: ILink; setIsOpen: (isOpen: boolean) => void }) {
const { update } = useLinks();
const { values, errors, handleChange, handleSubmit } = useFormik({
initialValues: {
url: link.url,
shorter_name: link.shorter_name,
id: link.id,
},
validationSchema: toFormikValidationSchema(LinkForUpdateSchema),
validateOnChange: false,
validateOnBlur: false,
onSubmit: async (values) => {
const dto = LinkForUpdateSchema.parse(values);

update(dto);
setIsOpen(false);
},
});
return (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit link</DialogTitle>
</DialogHeader>
<div>
<form id="edit-link" className="space-y-4" onSubmit={handleSubmit}>
<TextField
label="Shorter Name"
type="text"
id="shorter_name"
name="shorter_name"
placeholder="linx-short-url"
autoComplete="off"
value={values.shorter_name}
error={errors.shorter_name}
onChange={handleChange}
required
/>
<TextField
label="Redirect URL"
type="url"
id="url"
name="url"
placeholder="https://example.com/longlonglonglonglonglongurl"
value={values.url}
error={errors.url}
onChange={handleChange}
required
/>
</form>
</div>
<DialogFooter>
<Button form="edit-link" type="submit">
Edit Link
</Button>
</DialogFooter>
</DialogContent>
);
}
13 changes: 12 additions & 1 deletion client/src/context/LinksContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface LinksContextProps {
isLoading: boolean;
links: ILink[] | [];
create: (link: ILinkForCreate) => void;
update: (link: ILinkForCreate) => void;
deleteById: (link_id: string) => void;
}

Expand Down Expand Up @@ -45,6 +46,16 @@ export const LinksProvider = ({ children }: { children?: React.ReactNode }) => {
});
};

const update = async (linkDTO: ILinkForCreate) => {
const updated_link = await Services.link.update(linkDTO);
setLinks((oldLinks) => {
const newList = [...oldLinks];
const index = newList.findIndex((link) => link.id === updated_link.id);
newList[index] = updated_link;
return newList;
});
};

const deleteById = async (id: string) => {
const link_id = await Services.link.deleteById(id);

Expand All @@ -61,7 +72,7 @@ export const LinksProvider = ({ children }: { children?: React.ReactNode }) => {

return (
<LinksContext.Provider
value={{ isLoading: loading, links, create, deleteById }}
value={{ isLoading: loading, links, create, update, deleteById }}
>
{children}
</LinksContext.Provider>
Expand Down
15 changes: 15 additions & 0 deletions client/src/services/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ export default class Link {
}
}

static async update(link: ILinkForCreate) {
try {
const request = await fetch('/links/edit', {
method: 'PUT',
body: JSON.stringify(link),
});
const body = await request.json();

return LinkSchema.parse(body.data.link);
} catch (e) {
console.log('LinkService', e);
throw e;
}
}

static async deleteById(link_id: string) {
try {
const request = await fetch('/links/deleteById', {
Expand Down
17 changes: 17 additions & 0 deletions server/src/controllers/Link/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Services from '../../services';
import { BadRequestError } from '../../utils/errorHandler';

import type { ILinkForUpdate } from '@linx/shared';
import type { Reply, Request } from '../../types';

export default async function update(request: Request, reply: Reply) {
const data = request.body as ILinkForUpdate;
const user = request.user;

const link = await Services.link.update(user.id, data);
return reply.code(201).send({
success: true,
message: 'Link modified successfully',
data: { link: link },
});
}
11 changes: 10 additions & 1 deletion server/src/repository/Link.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import database from './database';

import type { ILink, ILinkForCreate } from '@linx/shared';
import type { ILink, ILinkForCreate, ILinkForUpdate } from '@linx/shared';

export class Link {
static async create(linkDTO: ILinkForCreate): Promise<ILink> {
Expand Down Expand Up @@ -31,6 +31,15 @@ export class Link {

return links;
}

static async update(linkId: string, linkDTO: ILinkForUpdate): Promise<ILink> {
const [link] = await database<ILink>('links')
.update({ ...linkDTO, updated_at: new Date() })
.where({ id: linkId })
.returning('*');
return link;
}

static async deleteById(linkId: string): Promise<string> {
const _link = await database<ILink>('links').where('id', linkId).delete();
return linkId;
Expand Down
15 changes: 14 additions & 1 deletion server/src/routes/link/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import create from '../../controllers/Link/create';
import checkJwt from '../../middlewares/checkJwt';

import { LinkForCreateSchema, LinkForDeleteSchema } from '@linx/shared';
import {
LinkForCreateSchema,
LinkForDeleteSchema,
LinkForUpdateSchema,
} from '@linx/shared';
import requestValidation from '../../utils/requestValidation';

import type {
DoneFuncWithErrOrRes,
FastifyInstance,
RegisterOptions,
} from 'fastify';
import { z } from 'zod';
import deleteById from '../../controllers/Link/delete';
import getAll from '../../controllers/Link/getAll';
import update from '../../controllers/Link/update';

export default function link(
fastify: FastifyInstance,
Expand All @@ -32,6 +38,13 @@ export default function link(
handler: create,
});

fastify.route({
method: 'PUT',
url: '/edit',
preHandler: requestValidation(LinkForUpdateSchema),
handler: update,
});

fastify.route({
method: 'DELETE',
url: '/deleteById',
Expand Down
25 changes: 24 additions & 1 deletion server/src/services/Link.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Repository from '../repository';
import { BadRequestError, UnauthorizedError } from '../utils/errorHandler';

import type { ILink, ILinkForCreate } from '@linx/shared';
import type { ILink, ILinkForCreate, ILinkForUpdate } from '@linx/shared';

export default class Link {
static async create(link_dto: ILinkForCreate): Promise<ILink> {
const check = await Repository.link.getByShorterName(link_dto.shorter_name);
// todo: improve this
if (check)
throw new BadRequestError('Exists the shorter name, please select other');

Expand All @@ -25,6 +26,28 @@ export default class Link {
return link;
}

static async update(user_id: string, link_dto: ILinkForUpdate) {
try {
const check = await Repository.link.getById(link_dto.id);
if (check.user_id !== user_id)
throw new UnauthorizedError('You no are the owner of this link');
const check_name = await Repository.link.getByShorterName(
link_dto.shorter_name,
);
// todo: improve this
if (check_name)
throw new BadRequestError(
'Exists the shorter name, please select other',
);

const link = await Repository.link.update(link_dto.id, link_dto);

return link;
} catch (error) {
console.log(error);
throw error;
}
}
static async deleteByIdUser(
link_id: string,
user_id: string,
Expand Down
6 changes: 6 additions & 0 deletions shared/src/interfaces/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export interface ILinkForCreate {
user_id?: string;
}

export interface ILinkForUpdate {
id: string;
url: string;
shorter_name: string;
}

export interface ILinkForDelete {
id: string;
}
6 changes: 6 additions & 0 deletions shared/src/schema/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export const LinkForCreateSchema = z.object({
shorter_name: z.string().max(20),
});

export const LinkForUpdateSchema = z.object({
id: z.string(),
url: z.string().url(),
shorter_name: z.string().max(20),
});

export const LinkForDeleteSchema = z.object({
id: z.string(),
});

0 comments on commit bd41465

Please sign in to comment.