LinksTable.tsx 12 KB

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