LinksTable.tsx 12 KB

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