Shortener.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import { CopyToClipboard } from "react-copy-to-clipboard";
  2. import { useFormState } from "react-use-form-state";
  3. import { Flex } from "reflexbox/styled-components";
  4. import React, { FC, useState } from "react";
  5. import styled from "styled-components";
  6. import getConfig from "next/config";
  7. import { useStoreActions, useStoreState } from "../store";
  8. import { Checkbox, Select, TextInput } from "./Input";
  9. import { Col, RowCenterH, RowCenter } from "./Layout";
  10. import { useMessage, useCopy } from "../hooks";
  11. import { removeProtocol } from "../utils";
  12. import Text, { H1, Span } from "./Text";
  13. import { Link } from "../store/links";
  14. import Animation from "./Animation";
  15. import { Colors } from "../consts";
  16. import Icon from "./Icon";
  17. const { publicRuntimeConfig } = getConfig();
  18. const SubmitIconWrapper = styled.div`
  19. content: "";
  20. position: absolute;
  21. top: 0;
  22. right: 12px;
  23. width: 64px;
  24. height: 100%;
  25. display: flex;
  26. justify-content: center;
  27. align-items: center;
  28. cursor: pointer;
  29. :hover svg {
  30. fill: #673ab7;
  31. }
  32. @media only screen and (max-width: 448px) {
  33. right: 8px;
  34. width: 40px;
  35. }
  36. `;
  37. const ShortenedLink = styled(H1)`
  38. cursor: "pointer";
  39. border-bottom: 1px dotted ${Colors.StatsTotalUnderline};
  40. cursor: pointer;
  41. :hover {
  42. opacity: 0.8;
  43. }
  44. `;
  45. interface Form {
  46. target: string;
  47. domain?: string;
  48. customurl?: string;
  49. password?: string;
  50. description?: string;
  51. showAdvanced?: boolean;
  52. }
  53. const defaultDomain = publicRuntimeConfig.DEFAULT_DOMAIN;
  54. const Shortener = () => {
  55. const { isAuthenticated } = useStoreState(s => s.auth);
  56. const domains = useStoreState(s => s.settings.domains);
  57. const submit = useStoreActions(s => s.links.submit);
  58. const [link, setLink] = useState<Link | null>(null);
  59. const [message, setMessage] = useMessage(3000);
  60. const [loading, setLoading] = useState(false);
  61. const [copied, setCopied] = useCopy();
  62. const [formState, { raw, password, text, select, label }] = useFormState<
  63. Form
  64. >(
  65. { showAdvanced: false },
  66. {
  67. withIds: true,
  68. onChange(e, stateValues, nextStateValues) {
  69. if (stateValues.showAdvanced && !nextStateValues.showAdvanced) {
  70. formState.clear();
  71. formState.setField("target", stateValues.target);
  72. }
  73. }
  74. }
  75. );
  76. const submitLink = async (reCaptchaToken?: string) => {
  77. try {
  78. const link = await submit({ ...formState.values, reCaptchaToken });
  79. setLink(link);
  80. formState.clear();
  81. } catch (err) {
  82. setMessage(
  83. err?.response?.data?.error || "Couldn't create the short link."
  84. );
  85. }
  86. setLoading(false);
  87. };
  88. const onSubmit = async e => {
  89. e.preventDefault();
  90. if (loading) return;
  91. setCopied(false);
  92. setLoading(true);
  93. if (process.env.NODE_ENV === "production" && !isAuthenticated) {
  94. window.grecaptcha.execute(window.captchaId);
  95. const getCaptchaToken = () => {
  96. setTimeout(() => {
  97. if (window.isCaptchaReady) {
  98. const reCaptchaToken = window.grecaptcha.getResponse(
  99. window.captchaId
  100. );
  101. window.isCaptchaReady = false;
  102. window.grecaptcha.reset(window.captchaId);
  103. return submitLink(reCaptchaToken);
  104. }
  105. return getCaptchaToken();
  106. }, 200);
  107. };
  108. return getCaptchaToken();
  109. }
  110. return submitLink();
  111. };
  112. const title = !link && (
  113. <H1 fontSize={[25, 27, 32]} light>
  114. Kutt your links{" "}
  115. <Span style={{ borderBottom: "2px dotted #999" }} light>
  116. shorter
  117. </Span>
  118. .
  119. </H1>
  120. );
  121. const result = link && (
  122. <Animation
  123. as={RowCenter}
  124. offset="-20px"
  125. duration="0.4s"
  126. style={{ position: "relative" }}
  127. >
  128. {copied ? (
  129. <Animation offset="10px" duration="0.2s" alignItems="center">
  130. <Icon
  131. size={[30, 35]}
  132. py={0}
  133. px={0}
  134. mr={3}
  135. p={["4px", "5px"]}
  136. name="check"
  137. strokeWidth="3"
  138. stroke={Colors.CheckIcon}
  139. />
  140. </Animation>
  141. ) : (
  142. <Animation offset="-10px" duration="0.2s">
  143. <CopyToClipboard text={link.link} onCopy={setCopied}>
  144. <Icon
  145. as="button"
  146. py={0}
  147. px={0}
  148. mr={3}
  149. size={[30, 35]}
  150. p={["6px", "7px"]}
  151. name="copy"
  152. strokeWidth="2.5"
  153. stroke={Colors.CopyIcon}
  154. backgroundColor={Colors.CopyIconBg}
  155. />
  156. </CopyToClipboard>
  157. </Animation>
  158. )}
  159. <CopyToClipboard text={link.link} onCopy={setCopied}>
  160. <ShortenedLink fontSize={[24, 26, 30]} pb="2px" light>
  161. {removeProtocol(link.link)}
  162. </ShortenedLink>
  163. </CopyToClipboard>
  164. </Animation>
  165. );
  166. return (
  167. <Col width={800} maxWidth="100%" px={[3]} flex="0 0 auto" mt={4}>
  168. <RowCenterH mb={[4, 48]}>
  169. {title}
  170. {result}
  171. </RowCenterH>
  172. <Flex
  173. as="form"
  174. id="shortenerform"
  175. width={1}
  176. alignItems="center"
  177. justifyContent="center"
  178. style={{ position: "relative" }}
  179. onSubmit={onSubmit}
  180. >
  181. <TextInput
  182. {...text("target")}
  183. placeholder="Paste your long URL"
  184. placeholderSize={[16, 17, 18]}
  185. fontSize={[18, 20, 22]}
  186. width={1}
  187. height={[58, 64, 72]}
  188. px={0}
  189. pr={[48, 84]}
  190. pl={[32, 40]}
  191. autoFocus
  192. data-lpignore
  193. />
  194. <SubmitIconWrapper onClick={onSubmit}>
  195. <Icon
  196. name={loading ? "spinner" : "send"}
  197. size={[22, 26, 28]}
  198. fill={loading ? "none" : "#aaa"}
  199. stroke={loading ? Colors.Spinner : "none"}
  200. mb={1}
  201. mr={1}
  202. />
  203. </SubmitIconWrapper>
  204. </Flex>
  205. {message.text && (
  206. <Text color={message.color} mt={24} mb={1} textAlign="center">
  207. {message.text}
  208. </Text>
  209. )}
  210. <Checkbox
  211. {...raw({
  212. name: "showAdvanced",
  213. onChange: e => {
  214. if (!isAuthenticated) {
  215. setMessage(
  216. "You need to log in or sign up to use advanced options."
  217. );
  218. return false;
  219. }
  220. return !formState.values.showAdvanced;
  221. }
  222. })}
  223. checked={formState.values.showAdvanced}
  224. label="Show advanced options"
  225. mt={[3, 24]}
  226. alignSelf="flex-start"
  227. />
  228. {formState.values.showAdvanced && (
  229. <div>
  230. <Flex mt={4} flexDirection={["column", "row"]}>
  231. <Col mb={[3, 0]}>
  232. <Text
  233. as="label"
  234. {...label("domain")}
  235. fontSize={[14, 15]}
  236. mb={2}
  237. bold
  238. >
  239. Domain
  240. </Text>
  241. <Select
  242. {...select("domain")}
  243. data-lpignore
  244. pl={[3, 24]}
  245. pr={[3, 24]}
  246. fontSize={[14, 15]}
  247. height={[40, 44]}
  248. width={[1, 210, 240]}
  249. options={[
  250. { key: defaultDomain, value: "" },
  251. ...domains.map(d => ({
  252. key: d.address,
  253. value: d.address
  254. }))
  255. ]}
  256. />
  257. </Col>
  258. <Col mb={[3, 0]} ml={[0, 24]}>
  259. <Text
  260. as="label"
  261. {...label("customurl")}
  262. fontSize={[14, 15]}
  263. mb={2}
  264. bold
  265. >
  266. {formState.values.domain || defaultDomain}/
  267. </Text>
  268. <TextInput
  269. {...text("customurl")}
  270. placeholder="Custom address..."
  271. autocomplete="off"
  272. data-lpignore
  273. pl={[3, 24]}
  274. pr={[3, 24]}
  275. placeholderSize={[13, 14]}
  276. fontSize={[14, 15]}
  277. height={[40, 44]}
  278. width={[1, 210, 240]}
  279. />
  280. </Col>
  281. <Col ml={[0, 24]}>
  282. <Text
  283. as="label"
  284. {...label("password")}
  285. fontSize={[14, 15]}
  286. mb={2}
  287. bold
  288. >
  289. Password:
  290. </Text>
  291. <TextInput
  292. {...password("password")}
  293. placeholder="Password..."
  294. autocomplete="off"
  295. data-lpignore
  296. pl={[3, 24]}
  297. pr={[3, 24]}
  298. placeholderSize={[13, 14]}
  299. fontSize={[14, 15]}
  300. height={[40, 44]}
  301. width={[1, 210, 240]}
  302. />
  303. </Col>
  304. </Flex>
  305. <Flex mt={[3]} flexDirection={["column", "row"]}>
  306. <Col width={1}>
  307. <Text
  308. as="description"
  309. {...label("description")}
  310. fontSize={[14, 15]}
  311. mb={2}
  312. bold
  313. >
  314. Description
  315. </Text>
  316. <TextInput
  317. {...text("description")}
  318. placeholder="Description"
  319. data-lpignore
  320. pl={[3, 24]}
  321. pr={[3, 24]}
  322. placeholderSize={[13, 14]}
  323. fontSize={[14, 15]}
  324. height={[40, 44]}
  325. width={1}
  326. maxWidth="100%"
  327. />
  328. </Col>
  329. </Flex>
  330. </div>
  331. )}
  332. </Col>
  333. );
  334. };
  335. export default Shortener;