Shortener.tsx 7.4 KB

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