LinksTable.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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 QRCode from "qrcode.react";
  8. import Link from "next/link";
  9. import { useStoreActions, useStoreState } from "../store";
  10. import { removeProtocol, withComma, errorMessage } from "../utils";
  11. import { Checkbox, TextInput } from "./Input";
  12. import { NavButton, Button } from "./Button";
  13. import { Col, RowCenter } from "./Layout";
  14. import Text, { H2, Span } from "./Text";
  15. import { ifProp } from "styled-tools";
  16. import Animation from "./Animation";
  17. import { Colors } from "../consts";
  18. import Tooltip from "./Tooltip";
  19. import Table from "./Table";
  20. import ALink from "./ALink";
  21. import Modal from "./Modal";
  22. import Icon from "./Icon";
  23. import { useMessage } from "../hooks";
  24. const Tr = styled(Flex).attrs({ as: "tr", px: [12, 12, 2] })``;
  25. const Th = styled(Flex)``;
  26. Th.defaultProps = { as: "th", flexBasis: 0, py: [12, 12, 3], px: [12, 12, 3] };
  27. const Td = styled(Flex)<{ withFade?: boolean }>`
  28. position: relative;
  29. white-space: nowrap;
  30. ${ifProp(
  31. "withFade",
  32. css`
  33. :after {
  34. content: "";
  35. position: absolute;
  36. right: 0;
  37. top: 0;
  38. height: 100%;
  39. width: 16px;
  40. background: linear-gradient(to left, white, white, transparent);
  41. }
  42. tr:hover &:after {
  43. background: linear-gradient(
  44. to left,
  45. ${Colors.TableRowHover},
  46. ${Colors.TableRowHover},
  47. transparent
  48. );
  49. }
  50. `
  51. )}
  52. `;
  53. Td.defaultProps = {
  54. as: "td",
  55. fontSize: [15, 16],
  56. alignItems: "center",
  57. flexBasis: 0,
  58. py: [12, 12, 3],
  59. px: [12, 12, 3]
  60. };
  61. const Action = (props: React.ComponentProps<typeof Icon>) => (
  62. <Icon
  63. as="button"
  64. py={0}
  65. px={0}
  66. mr={2}
  67. size={[23, 24]}
  68. p={["4px", "5px"]}
  69. stroke="#666"
  70. {...props}
  71. />
  72. );
  73. const ogLinkFlex = { flexGrow: [1, 3, 7], flexShrink: [1, 3, 7] };
  74. const createdFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
  75. const shortLinkFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
  76. const viewsFlex = {
  77. flexGrow: [0.5, 0.5, 1],
  78. flexShrink: [0.5, 0.5, 1],
  79. justifyContent: "flex-end"
  80. };
  81. const actionsFlex = { flexGrow: [1, 1, 2.5], flexShrink: [1, 1, 2.5] };
  82. interface Form {
  83. all: boolean;
  84. limit: string;
  85. skip: string;
  86. search: string;
  87. }
  88. const LinksTable: FC = () => {
  89. const isAdmin = useStoreState(s => s.auth.isAdmin);
  90. const links = useStoreState(s => s.links);
  91. const { get, deleteOne } = useStoreActions(s => s.links);
  92. const [tableMessage, setTableMessage] = useState("No links to show.");
  93. const [copied, setCopied] = useState([]);
  94. const [qrModal, setQRModal] = useState(-1);
  95. const [deleteModal, setDeleteModal] = useState(-1);
  96. const [deleteLoading, setDeleteLoading] = useState(false);
  97. const [deleteMessage, setDeleteMessage] = useMessage();
  98. const [formState, { label, checkbox, text }] = useFormState<Form>(
  99. { skip: "0", limit: "10", all: false },
  100. { withIds: true }
  101. );
  102. const options = formState.values;
  103. const linkToDelete = links.items[deleteModal];
  104. useEffect(() => {
  105. get(options).catch(err => setTableMessage(err?.response?.data?.error));
  106. }, [options.limit, options.skip, options.all]);
  107. const onSubmit = e => {
  108. e.preventDefault();
  109. get(options);
  110. };
  111. const onCopy = (index: number) => () => {
  112. setCopied([index]);
  113. setTimeout(() => {
  114. setCopied(s => s.filter(i => i !== index));
  115. }, 1500);
  116. };
  117. const onDelete = async () => {
  118. setDeleteLoading(true);
  119. try {
  120. await deleteOne({
  121. id: linkToDelete.address,
  122. domain: linkToDelete.domain
  123. });
  124. await get(options);
  125. setDeleteModal(-1);
  126. } catch (err) {
  127. setDeleteMessage(errorMessage(err));
  128. }
  129. setDeleteLoading(false);
  130. };
  131. const onNavChange = (nextPage: number) => () => {
  132. formState.setField("skip", (parseInt(options.skip) + nextPage).toString());
  133. };
  134. const Nav = (
  135. <Th
  136. alignItems="center"
  137. justifyContent="flex-end"
  138. flexGrow={1}
  139. flexShrink={1}
  140. >
  141. <Flex as="ul" m={0} p={0} style={{ listStyle: "none" }}>
  142. {["10", "25", "50"].map(c => (
  143. <Flex key={c} ml={[10, 12]}>
  144. <NavButton
  145. disabled={options.limit === c}
  146. onClick={() => {
  147. formState.setField("limit", c);
  148. formState.setField("skip", "0");
  149. }}
  150. >
  151. {c}
  152. </NavButton>
  153. </Flex>
  154. ))}
  155. </Flex>
  156. <Flex
  157. width="1px"
  158. height={20}
  159. mx={[3, 24]}
  160. style={{ backgroundColor: "#ccc" }}
  161. />
  162. <Flex>
  163. <NavButton
  164. onClick={onNavChange(-parseInt(options.limit))}
  165. disabled={options.skip === "0"}
  166. px={2}
  167. >
  168. <Icon name="chevronLeft" size={15} />
  169. </NavButton>
  170. <NavButton
  171. onClick={onNavChange(parseInt(options.limit))}
  172. disabled={
  173. parseInt(options.skip) + parseInt(options.limit) > links.total
  174. }
  175. ml={12}
  176. px={2}
  177. >
  178. <Icon name="chevronRight" size={15} />
  179. </NavButton>
  180. </Flex>
  181. </Th>
  182. );
  183. return (
  184. <Col width={1200} maxWidth="95%" margin="40px 0 120px" my={6}>
  185. <H2 mb={3} light>
  186. Recent shortened links.
  187. </H2>
  188. <Table scrollWidth="800px">
  189. <thead>
  190. <Tr justifyContent="space-between">
  191. <Th flexGrow={1} flexShrink={1}>
  192. <Flex as="form" onSubmit={onSubmit}>
  193. <TextInput
  194. {...text("search")}
  195. placeholder="Search..."
  196. height={[30, 32]}
  197. placeholderSize={[13, 13, 13, 13]}
  198. fontSize={[14]}
  199. pl={12}
  200. pr={12}
  201. width={[1]}
  202. br="3px"
  203. bbw="2px"
  204. />
  205. {isAdmin && (
  206. <Checkbox
  207. {...label("all")}
  208. {...checkbox("all")}
  209. label="All links"
  210. ml={3}
  211. fontSize={[14, 15]}
  212. width={[15, 16]}
  213. height={[15, 16]}
  214. />
  215. )}
  216. </Flex>
  217. </Th>
  218. {Nav}
  219. </Tr>
  220. <Tr>
  221. <Th {...ogLinkFlex}>Original URL</Th>
  222. <Th {...createdFlex}>Created</Th>
  223. <Th {...shortLinkFlex}>Short URL</Th>
  224. <Th {...viewsFlex}>Views</Th>
  225. <Th {...actionsFlex}></Th>
  226. </Tr>
  227. </thead>
  228. <tbody style={{ opacity: links.loading ? 0.4 : 1 }}>
  229. {!links.items.length ? (
  230. <Tr width={1} justifyContent="center">
  231. <Td flex="1 1 auto" justifyContent="center">
  232. <Text fontSize={18} light>
  233. {links.loading ? "Loading links..." : tableMessage}
  234. </Text>
  235. </Td>
  236. </Tr>
  237. ) : (
  238. <>
  239. {links.items.map((l, index) => (
  240. <Tr key={`link-${index}`}>
  241. <Td {...ogLinkFlex} withFade>
  242. <ALink href={l.target}>{l.target}</ALink>
  243. </Td>
  244. <Td {...createdFlex}>{`${formatDistanceToNow(
  245. new Date(l.created_at)
  246. )} ago`}</Td>
  247. <Td {...shortLinkFlex} withFade>
  248. {copied.includes(index) ? (
  249. <Animation
  250. minWidth={32}
  251. offset="10px"
  252. duration="0.2s"
  253. alignItems="center"
  254. >
  255. <Icon
  256. size={[23, 24]}
  257. py={0}
  258. px={0}
  259. mr={2}
  260. p="3px"
  261. name="check"
  262. strokeWidth="3"
  263. stroke={Colors.CheckIcon}
  264. />
  265. </Animation>
  266. ) : (
  267. <Animation minWidth={32} offset="-10px" duration="0.2s">
  268. <CopyToClipboard text={l.link} onCopy={onCopy(index)}>
  269. <Action
  270. name="copy"
  271. strokeWidth="2.5"
  272. stroke={Colors.CopyIcon}
  273. backgroundColor={Colors.CopyIconBg}
  274. />
  275. </CopyToClipboard>
  276. </Animation>
  277. )}
  278. <ALink href={l.link}>{removeProtocol(l.link)}</ALink>
  279. </Td>
  280. <Td {...viewsFlex}>{withComma(l.visit_count)}</Td>
  281. <Td {...actionsFlex} justifyContent="flex-end">
  282. {l.password && (
  283. <>
  284. <Tooltip id={`${index}-tooltip-password`}>
  285. Password protected
  286. </Tooltip>
  287. <Action
  288. as="span"
  289. data-tip
  290. data-for={`${index}-tooltip-password`}
  291. name="key"
  292. stroke="#bbb"
  293. strokeWidth="2.5"
  294. backgroundColor="none"
  295. />
  296. </>
  297. )}
  298. {l.visit_count > 0 && (
  299. <Link
  300. href={`/stats?id=${l.id}${
  301. l.domain ? `&domain=${l.domain}` : ""
  302. }`}
  303. >
  304. <ALink title="View stats" forButton>
  305. <Action
  306. name="pieChart"
  307. stroke={Colors.PieIcon}
  308. strokeWidth="2.5"
  309. backgroundColor={Colors.PieIconBg}
  310. />
  311. </ALink>
  312. </Link>
  313. )}
  314. <Action
  315. name="qrcode"
  316. stroke="none"
  317. fill={Colors.QrCodeIcon}
  318. backgroundColor={Colors.QrCodeIconBg}
  319. onClick={() => setQRModal(index)}
  320. />
  321. <Action
  322. mr={0}
  323. name="trash"
  324. strokeWidth="2"
  325. stroke={Colors.TrashIcon}
  326. backgroundColor={Colors.TrashIconBg}
  327. onClick={() => setDeleteModal(index)}
  328. />
  329. </Td>
  330. </Tr>
  331. ))}
  332. </>
  333. )}
  334. </tbody>
  335. <tfoot>
  336. <Tr justifyContent="flex-end">{Nav}</Tr>
  337. </tfoot>
  338. </Table>
  339. <Modal
  340. id="table-qrcode-modal"
  341. minWidth="max-content"
  342. show={qrModal > -1}
  343. closeHandler={() => setQRModal(-1)}
  344. >
  345. {links.items[qrModal] && (
  346. <RowCenter width={192}>
  347. <QRCode size={192} value={links.items[qrModal].link} />
  348. </RowCenter>
  349. )}
  350. </Modal>
  351. <Modal
  352. id="delete-custom-domain"
  353. show={deleteModal > -1}
  354. closeHandler={() => setDeleteModal(-1)}
  355. >
  356. {linkToDelete && (
  357. <>
  358. <H2 mb={24} textAlign="center" bold>
  359. Delete link?
  360. </H2>
  361. <Text textAlign="center">
  362. Are you sure do you want to delete the link{" "}
  363. <Span bold>"{removeProtocol(linkToDelete.link)}"</Span>?
  364. </Text>
  365. <Flex justifyContent="center" mt={44}>
  366. {deleteLoading ? (
  367. <>
  368. <Icon name="spinner" size={20} stroke={Colors.Spinner} />
  369. </>
  370. ) : deleteMessage.text ? (
  371. <Text fontSize={15} color={deleteMessage.color}>
  372. {deleteMessage.text}
  373. </Text>
  374. ) : (
  375. <>
  376. <Button
  377. color="gray"
  378. mr={3}
  379. onClick={() => setDeleteModal(-1)}
  380. >
  381. Cancel
  382. </Button>
  383. <Button color="red" ml={3} onClick={onDelete}>
  384. <Icon name="trash" stroke="white" mr={2} />
  385. Delete
  386. </Button>
  387. </>
  388. )}
  389. </Flex>
  390. </>
  391. )}
  392. </Modal>
  393. </Col>
  394. );
  395. };
  396. export default LinksTable;