LinksTable.tsx 22 KB

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