LinksTable.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. import formatDistanceToNow from "date-fns/formatDistanceToNow";
  2. import { CopyToClipboard } from "react-copy-to-clipboard";
  3. import React, { FC, useState, useEffect } from "react";
  4. import { useFormState } from "react-use-form-state";
  5. import { Flex } from "reflexbox/styled-components";
  6. import styled, { css } from "styled-components";
  7. import { ifProp } from "styled-tools";
  8. import getConfig from "next/config";
  9. import QRCode from "qrcode.react";
  10. import Link from "next/link";
  11. import differenceInMilliseconds from "date-fns/differenceInMilliseconds";
  12. import ms from "ms";
  13. import { removeProtocol, withComma, errorMessage } from "../utils";
  14. import { useStoreActions, useStoreState } from "../store";
  15. import { Link as LinkType } from "../store/links";
  16. import { Checkbox, TextInput } from "./Input";
  17. import { NavButton, Button } from "./Button";
  18. import { Col, RowCenter } from "./Layout";
  19. import Text, { H2, Span } from "./Text";
  20. import { useMessage } from "../hooks";
  21. import Animation from "./Animation";
  22. import { Colors } from "../consts";
  23. import Tooltip from "./Tooltip";
  24. import Table from "./Table";
  25. import ALink from "./ALink";
  26. import Modal from "./Modal";
  27. import Icon from "./Icon";
  28. const { publicRuntimeConfig } = getConfig();
  29. const Tr = styled(Flex).attrs({ as: "tr", px: [12, 12, 2] })``;
  30. const Th = styled(Flex)``;
  31. Th.defaultProps = { as: "th", flexBasis: 0, py: [12, 12, 3], px: [12, 12, 3] };
  32. const Td = styled(Flex)<{ withFade?: boolean }>`
  33. position: relative;
  34. white-space: nowrap;
  35. ${ifProp(
  36. "withFade",
  37. css`
  38. :after {
  39. content: "";
  40. position: absolute;
  41. right: 0;
  42. top: 0;
  43. height: 100%;
  44. width: 16px;
  45. background: linear-gradient(to left, white, rgba(255, 255, 255, 0.001));
  46. }
  47. tr:hover &:after {
  48. background: linear-gradient(
  49. to left,
  50. ${Colors.TableRowHover},
  51. rgba(255, 255, 255, 0.001)
  52. );
  53. }
  54. `
  55. )}
  56. `;
  57. Td.defaultProps = {
  58. as: "td",
  59. fontSize: [15, 16],
  60. alignItems: "center",
  61. flexBasis: 0,
  62. py: [12, 12, 3],
  63. px: [12, 12, 3]
  64. };
  65. const EditContent = styled(Col)`
  66. border-bottom: 1px solid ${Colors.TableRowHover};
  67. background-color: #fafafa;
  68. `;
  69. const Action = (props: React.ComponentProps<typeof Icon>) => (
  70. <Icon
  71. as="button"
  72. py={0}
  73. px={0}
  74. mr={2}
  75. size={[23, 24]}
  76. flexShrink={0}
  77. p={["4px", "5px"]}
  78. stroke="#666"
  79. {...props}
  80. />
  81. );
  82. const ogLinkFlex = { flexGrow: [1, 3, 7], flexShrink: [1, 3, 7] };
  83. const createdFlex = { flexGrow: [1, 1, 2.5], flexShrink: [1, 1, 2.5] };
  84. const shortLinkFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
  85. const viewsFlex = {
  86. flexGrow: [0.5, 0.5, 1],
  87. flexShrink: [0.5, 0.5, 1],
  88. justifyContent: "flex-end"
  89. };
  90. const actionsFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
  91. interface RowProps {
  92. index: number;
  93. link: LinkType;
  94. setDeleteModal: (number) => void;
  95. }
  96. interface BanForm {
  97. host: boolean;
  98. user: boolean;
  99. userLinks: boolean;
  100. domain: boolean;
  101. }
  102. interface EditForm {
  103. target: string;
  104. address: string;
  105. description?: string;
  106. expire_in?: string;
  107. password?: string;
  108. }
  109. const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
  110. const isAdmin = useStoreState(s => s.auth.isAdmin);
  111. const ban = useStoreActions(s => s.links.ban);
  112. const edit = useStoreActions(s => s.links.edit);
  113. const [banFormState, { checkbox }] = useFormState<BanForm>();
  114. const [editFormState, { text, label, password }] = useFormState<EditForm>(
  115. {
  116. target: link.target,
  117. address: link.address,
  118. description: link.description,
  119. expire_in: link.expire_in
  120. ? ms(differenceInMilliseconds(new Date(link.expire_in), new Date()), {
  121. long: true
  122. })
  123. : "",
  124. password: ""
  125. },
  126. { withIds: true }
  127. );
  128. const [copied, setCopied] = useState(false);
  129. const [showEdit, setShowEdit] = useState(false);
  130. const [qrModal, setQRModal] = useState(false);
  131. const [banModal, setBanModal] = useState(false);
  132. const [banLoading, setBanLoading] = useState(false);
  133. const [banMessage, setBanMessage] = useMessage();
  134. const [editLoading, setEditLoading] = useState(false);
  135. const [editMessage, setEditMessage] = useMessage();
  136. const onCopy = () => {
  137. setCopied(true);
  138. setTimeout(() => {
  139. setCopied(false);
  140. }, 1500);
  141. };
  142. const onBan = async () => {
  143. setBanLoading(true);
  144. try {
  145. const res = await ban({ id: link.id, ...banFormState.values });
  146. setBanMessage(res.message, "green");
  147. setTimeout(() => {
  148. setBanModal(false);
  149. }, 2000);
  150. } catch (err) {
  151. setBanMessage(errorMessage(err));
  152. }
  153. setBanLoading(false);
  154. };
  155. const onEdit = async () => {
  156. if (editLoading) return;
  157. setEditLoading(true);
  158. try {
  159. await edit({ id: link.id, ...editFormState.values });
  160. setShowEdit(false);
  161. } catch (err) {
  162. setEditMessage(errorMessage(err));
  163. }
  164. editFormState.setField("password", "");
  165. setEditLoading(false);
  166. };
  167. const toggleEdit = () => {
  168. setShowEdit(s => !s);
  169. if (showEdit) editFormState.reset();
  170. setEditMessage("");
  171. };
  172. return (
  173. <>
  174. <Tr key={link.id}>
  175. <Td {...ogLinkFlex} withFade>
  176. <Col alignItems="flex-start">
  177. <ALink href={link.target}>{link.target}</ALink>
  178. {link.description && (
  179. <Text fontSize={[13, 14]} color="#888">
  180. {link.description}
  181. </Text>
  182. )}
  183. </Col>
  184. </Td>
  185. <Td {...createdFlex} flexDirection="column" alignItems="flex-start">
  186. <Text>{formatDistanceToNow(new Date(link.created_at))} ago</Text>
  187. {link.expire_in && (
  188. <Text fontSize={[13, 14]} color="#888">
  189. Expires in{" "}
  190. {ms(
  191. differenceInMilliseconds(new Date(link.expire_in), new Date()),
  192. {
  193. long: true
  194. }
  195. )}
  196. </Text>
  197. )}
  198. </Td>
  199. <Td {...shortLinkFlex} withFade>
  200. {copied ? (
  201. <Animation
  202. minWidth={32}
  203. offset="10px"
  204. duration="0.2s"
  205. alignItems="center"
  206. >
  207. <Icon
  208. size={[23, 24]}
  209. py={0}
  210. px={0}
  211. mr={2}
  212. p="3px"
  213. name="check"
  214. strokeWidth="3"
  215. stroke={Colors.CheckIcon}
  216. />
  217. </Animation>
  218. ) : (
  219. <Animation minWidth={32} offset="-10px" duration="0.2s">
  220. <CopyToClipboard text={link.link} onCopy={onCopy}>
  221. <Action
  222. name="copy"
  223. strokeWidth="2.5"
  224. stroke={Colors.CopyIcon}
  225. backgroundColor={Colors.CopyIconBg}
  226. />
  227. </CopyToClipboard>
  228. </Animation>
  229. )}
  230. <ALink href={link.link}>{removeProtocol(link.link)}</ALink>
  231. </Td>
  232. <Td {...viewsFlex}>{withComma(link.visit_count)}</Td>
  233. <Td {...actionsFlex} justifyContent="flex-end">
  234. {link.password && (
  235. <>
  236. <Tooltip id={`${index}-tooltip-password`}>
  237. Password protected
  238. </Tooltip>
  239. <Action
  240. as="span"
  241. data-tip
  242. data-for={`${index}-tooltip-password`}
  243. name="key"
  244. stroke={"#bbb"}
  245. strokeWidth="2.5"
  246. backgroundColor="none"
  247. />
  248. </>
  249. )}
  250. {link.banned && (
  251. <>
  252. <Tooltip id={`${index}-tooltip-banned`}>Banned</Tooltip>
  253. <Action
  254. as="span"
  255. data-tip
  256. data-for={`${index}-tooltip-banned`}
  257. name="stop"
  258. stroke="#bbb"
  259. strokeWidth="2.5"
  260. backgroundColor="none"
  261. />
  262. </>
  263. )}
  264. {link.visit_count > 0 && (
  265. <Link href={`/stats?id=${link.id}`}>
  266. <ALink title="View stats" forButton>
  267. <Action
  268. name="pieChart"
  269. stroke={Colors.PieIcon}
  270. strokeWidth="2.5"
  271. backgroundColor={Colors.PieIconBg}
  272. />
  273. </ALink>
  274. </Link>
  275. )}
  276. <Action
  277. name="qrcode"
  278. stroke="none"
  279. fill={Colors.QrCodeIcon}
  280. backgroundColor={Colors.QrCodeIconBg}
  281. onClick={() => setQRModal(true)}
  282. />
  283. <Action
  284. name="editAlt"
  285. strokeWidth="2.5"
  286. stroke={Colors.EditIcon}
  287. backgroundColor={Colors.EditIconBg}
  288. onClick={toggleEdit}
  289. />
  290. {isAdmin && !link.banned && (
  291. <Action
  292. name="stop"
  293. strokeWidth="2"
  294. stroke={Colors.StopIcon}
  295. backgroundColor={Colors.StopIconBg}
  296. onClick={() => setBanModal(true)}
  297. />
  298. )}
  299. <Action
  300. mr={0}
  301. name="trash"
  302. strokeWidth="2"
  303. stroke={Colors.TrashIcon}
  304. backgroundColor={Colors.TrashIconBg}
  305. onClick={() => setDeleteModal(index)}
  306. />
  307. </Td>
  308. </Tr>
  309. {showEdit && (
  310. <EditContent as="tr">
  311. <Col
  312. as="td"
  313. alignItems="flex-start"
  314. px={[3, 3, 24]}
  315. py={[3, 3, 24]}
  316. width={1}
  317. >
  318. <Flex alignItems="flex-start" width={1}>
  319. <Col alignItems="flex-start" mr={3}>
  320. <Text
  321. {...label("target")}
  322. as="label"
  323. mb={2}
  324. fontSize={[14, 15]}
  325. bold
  326. >
  327. Target:
  328. </Text>
  329. <Flex as="form">
  330. <TextInput
  331. {...text("target")}
  332. placeholder="Target..."
  333. placeholderSize={[13, 14]}
  334. fontSize={[14, 15]}
  335. height={[40, 44]}
  336. width={[1, 300, 420]}
  337. pl={[3, 24]}
  338. pr={[3, 24]}
  339. required
  340. />
  341. </Flex>
  342. </Col>
  343. <Col alignItems="flex-start" mr={3}>
  344. <Text
  345. {...label("address")}
  346. as="label"
  347. mb={2}
  348. fontSize={[14, 15]}
  349. bold
  350. >
  351. {link.domain || publicRuntimeConfig.DEFAULT_DOMAIN}/
  352. </Text>
  353. <Flex as="form">
  354. <TextInput
  355. {...text("address")}
  356. placeholder="Custom address..."
  357. placeholderSize={[13, 14]}
  358. fontSize={[14, 15]}
  359. height={[40, 44]}
  360. width={[1, 210, 240]}
  361. pl={[3, 24]}
  362. pr={[3, 24]}
  363. required
  364. />
  365. </Flex>
  366. </Col>
  367. <Col alignItems="flex-start">
  368. <Text
  369. {...label("password")}
  370. as="label"
  371. mb={2}
  372. fontSize={[14, 15]}
  373. bold
  374. >
  375. Password
  376. </Text>
  377. <Flex as="form">
  378. <TextInput
  379. {...password({
  380. name: "password"
  381. })}
  382. placeholder={link.password ? "●●●●●●●" : "Password..."}
  383. autocomplete="off"
  384. data-lpignore
  385. pl={[3, 24]}
  386. pr={[3, 24]}
  387. placeholderSize={[13, 14]}
  388. fontSize={[14, 15]}
  389. height={[40, 44]}
  390. width={[1, 210, 240]}
  391. />
  392. </Flex>
  393. </Col>
  394. </Flex>
  395. <Flex alignItems="flex-start" width={1} mt={3}>
  396. <Col alignItems="flex-start" mr={3}>
  397. <Text
  398. {...label("description")}
  399. as="label"
  400. mb={2}
  401. fontSize={[14, 15]}
  402. bold
  403. >
  404. Description:
  405. </Text>
  406. <Flex as="form">
  407. <TextInput
  408. {...text("description")}
  409. placeholder="description..."
  410. placeholderSize={[13, 14]}
  411. fontSize={[14, 15]}
  412. height={[40, 44]}
  413. width={[1, 300, 420]}
  414. pl={[3, 24]}
  415. pr={[3, 24]}
  416. required
  417. />
  418. </Flex>
  419. </Col>
  420. <Col alignItems="flex-start">
  421. <Text
  422. {...label("expire_in")}
  423. as="label"
  424. mb={2}
  425. fontSize={[14, 15]}
  426. bold
  427. >
  428. Expire in:
  429. </Text>
  430. <Flex as="form">
  431. <TextInput
  432. {...text("expire_in")}
  433. placeholder="2 minutes/hours/days"
  434. placeholderSize={[13, 14]}
  435. fontSize={[14, 15]}
  436. height={[40, 44]}
  437. width={[1, 210, 240]}
  438. pl={[3, 24]}
  439. pr={[3, 24]}
  440. required
  441. />
  442. </Flex>
  443. </Col>
  444. </Flex>
  445. <Button
  446. color="blue"
  447. mt={3}
  448. height={[30, 38]}
  449. disabled={editLoading}
  450. onClick={onEdit}
  451. >
  452. <Icon
  453. name={editLoading ? "spinner" : "refresh"}
  454. stroke="white"
  455. mr={2}
  456. />
  457. {editLoading ? "Updating..." : "Update"}
  458. </Button>
  459. {editMessage.text && (
  460. <Text mt={3} fontSize={15} color={editMessage.color}>
  461. {editMessage.text}
  462. </Text>
  463. )}
  464. </Col>
  465. </EditContent>
  466. )}
  467. <Modal
  468. id="table-qrcode-modal"
  469. minWidth="max-content"
  470. show={qrModal}
  471. closeHandler={() => setQRModal(false)}
  472. >
  473. <RowCenter width={192}>
  474. <QRCode size={192} value={link.link} />
  475. </RowCenter>
  476. </Modal>
  477. <Modal
  478. id="table-ban-modal"
  479. show={banModal}
  480. closeHandler={() => setBanModal(false)}
  481. >
  482. <>
  483. <H2 mb={24} textAlign="center" bold>
  484. Ban link?
  485. </H2>
  486. <Text mb={24} textAlign="center">
  487. Are you sure do you want to ban the link{" "}
  488. <Span bold>"{removeProtocol(link.link)}"</Span>?
  489. </Text>
  490. <RowCenter>
  491. <Checkbox {...checkbox("user")} label="User" mb={12} />
  492. <Checkbox {...checkbox("userLinks")} label="User links" mb={12} />
  493. <Checkbox {...checkbox("host")} label="Host" mb={12} />
  494. <Checkbox {...checkbox("domain")} label="Domain" mb={12} />
  495. </RowCenter>
  496. <Flex justifyContent="center" mt={4}>
  497. {banLoading ? (
  498. <>
  499. <Icon name="spinner" size={20} stroke={Colors.Spinner} />
  500. </>
  501. ) : banMessage.text ? (
  502. <Text fontSize={15} color={banMessage.color}>
  503. {banMessage.text}
  504. </Text>
  505. ) : (
  506. <>
  507. <Button color="gray" mr={3} onClick={() => setBanModal(false)}>
  508. Cancel
  509. </Button>
  510. <Button color="red" ml={3} onClick={onBan}>
  511. <Icon name="stop" stroke="white" mr={2} />
  512. Ban
  513. </Button>
  514. </>
  515. )}
  516. </Flex>
  517. </>
  518. </Modal>
  519. </>
  520. );
  521. };
  522. interface Form {
  523. all: boolean;
  524. limit: string;
  525. skip: string;
  526. search: string;
  527. }
  528. const LinksTable: FC = () => {
  529. const isAdmin = useStoreState(s => s.auth.isAdmin);
  530. const links = useStoreState(s => s.links);
  531. const { get, remove } = useStoreActions(s => s.links);
  532. const [tableMessage, setTableMessage] = useState("No links to show.");
  533. const [deleteModal, setDeleteModal] = useState(-1);
  534. const [deleteLoading, setDeleteLoading] = useState(false);
  535. const [deleteMessage, setDeleteMessage] = useMessage();
  536. const [formState, { label, checkbox, text }] = useFormState<Form>(
  537. { skip: "0", limit: "10", all: false },
  538. { withIds: true }
  539. );
  540. const options = formState.values;
  541. const linkToDelete = links.items[deleteModal];
  542. useEffect(() => {
  543. get(options).catch(err =>
  544. setTableMessage(err?.response?.data?.error || "An error occurred.")
  545. );
  546. }, [options.limit, options.skip, options.all]);
  547. const onSubmit = e => {
  548. e.preventDefault();
  549. get(options);
  550. };
  551. const onDelete = async () => {
  552. setDeleteLoading(true);
  553. try {
  554. await remove(linkToDelete.id);
  555. await get(options);
  556. setDeleteModal(-1);
  557. } catch (err) {
  558. setDeleteMessage(errorMessage(err));
  559. }
  560. setDeleteLoading(false);
  561. };
  562. const onNavChange = (nextPage: number) => () => {
  563. formState.setField("skip", (parseInt(options.skip) + nextPage).toString());
  564. };
  565. const Nav = (
  566. <Th
  567. alignItems="center"
  568. justifyContent="flex-end"
  569. flexGrow={1}
  570. flexShrink={1}
  571. >
  572. <Flex as="ul" m={0} p={0} style={{ listStyle: "none" }}>
  573. {["10", "25", "50"].map(c => (
  574. <Flex key={c} ml={[10, 12]}>
  575. <NavButton
  576. disabled={options.limit === c}
  577. onClick={() => {
  578. formState.setField("limit", c);
  579. formState.setField("skip", "0");
  580. }}
  581. >
  582. {c}
  583. </NavButton>
  584. </Flex>
  585. ))}
  586. </Flex>
  587. <Flex
  588. width="1px"
  589. height={20}
  590. mx={[3, 24]}
  591. style={{ backgroundColor: "#ccc" }}
  592. />
  593. <Flex>
  594. <NavButton
  595. onClick={onNavChange(-parseInt(options.limit))}
  596. disabled={options.skip === "0"}
  597. px={2}
  598. >
  599. <Icon name="chevronLeft" size={15} />
  600. </NavButton>
  601. <NavButton
  602. onClick={onNavChange(parseInt(options.limit))}
  603. disabled={
  604. parseInt(options.skip) + parseInt(options.limit) > links.total
  605. }
  606. ml={12}
  607. px={2}
  608. >
  609. <Icon name="chevronRight" size={15} />
  610. </NavButton>
  611. </Flex>
  612. </Th>
  613. );
  614. return (
  615. <Col width={1200} maxWidth="95%" margin="40px 0 120px" my={6}>
  616. <H2 mb={3} light>
  617. Recent shortened links.
  618. </H2>
  619. <Table scrollWidth="1000px">
  620. <thead>
  621. <Tr justifyContent="space-between">
  622. <Th flexGrow={1} flexShrink={1}>
  623. <Flex as="form" onSubmit={onSubmit}>
  624. <TextInput
  625. {...text("search")}
  626. placeholder="Search..."
  627. height={[30, 32]}
  628. placeholderSize={[13, 13, 13, 13]}
  629. fontSize={[14]}
  630. pl={12}
  631. pr={12}
  632. width={[1]}
  633. br="3px"
  634. bbw="2px"
  635. />
  636. {isAdmin && (
  637. <Checkbox
  638. {...label("all")}
  639. {...checkbox("all")}
  640. label="All links"
  641. ml={3}
  642. fontSize={[14, 15]}
  643. width={[15, 16]}
  644. height={[15, 16]}
  645. />
  646. )}
  647. </Flex>
  648. </Th>
  649. {Nav}
  650. </Tr>
  651. <Tr>
  652. <Th {...ogLinkFlex}>Original URL</Th>
  653. <Th {...createdFlex}>Created</Th>
  654. <Th {...shortLinkFlex}>Short URL</Th>
  655. <Th {...viewsFlex}>Views</Th>
  656. <Th {...actionsFlex}></Th>
  657. </Tr>
  658. </thead>
  659. <tbody style={{ opacity: links.loading ? 0.4 : 1 }}>
  660. {!links.items.length ? (
  661. <Tr width={1} justifyContent="center">
  662. <Td flex="1 1 auto" justifyContent="center">
  663. <Text fontSize={18} light>
  664. {links.loading ? "Loading links..." : tableMessage}
  665. </Text>
  666. </Td>
  667. </Tr>
  668. ) : (
  669. <>
  670. {links.items.map((link, index) => (
  671. <Row
  672. setDeleteModal={setDeleteModal}
  673. index={index}
  674. link={link}
  675. key={link.id}
  676. />
  677. ))}
  678. </>
  679. )}
  680. </tbody>
  681. <tfoot>
  682. <Tr justifyContent="flex-end">{Nav}</Tr>
  683. </tfoot>
  684. </Table>
  685. <Modal
  686. id="delete-custom-domain"
  687. show={deleteModal > -1}
  688. closeHandler={() => setDeleteModal(-1)}
  689. >
  690. {linkToDelete && (
  691. <>
  692. <H2 mb={24} textAlign="center" bold>
  693. Delete link?
  694. </H2>
  695. <Text textAlign="center">
  696. Are you sure do you want to delete the link{" "}
  697. <Span bold>"{removeProtocol(linkToDelete.link)}"</Span>?
  698. </Text>
  699. <Flex justifyContent="center" mt={44}>
  700. {deleteLoading ? (
  701. <>
  702. <Icon name="spinner" size={20} stroke={Colors.Spinner} />
  703. </>
  704. ) : deleteMessage.text ? (
  705. <Text fontSize={15} color={deleteMessage.color}>
  706. {deleteMessage.text}
  707. </Text>
  708. ) : (
  709. <>
  710. <Button
  711. color="gray"
  712. mr={3}
  713. onClick={() => setDeleteModal(-1)}
  714. >
  715. Cancel
  716. </Button>
  717. <Button color="red" ml={3} onClick={onDelete}>
  718. <Icon name="trash" stroke="white" mr={2} />
  719. Delete
  720. </Button>
  721. </>
  722. )}
  723. </Flex>
  724. </>
  725. )}
  726. </Modal>
  727. </Col>
  728. );
  729. };
  730. export default LinksTable;