Shortener.tsx 8.4 KB

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