import formatDistanceToNow from "date-fns/formatDistanceToNow"; import { CopyToClipboard } from "react-copy-to-clipboard"; import React, { FC, useState, useEffect } from "react"; import { useFormState } from "react-use-form-state"; import { Flex } from "reflexbox/styled-components"; import styled, { css } from "styled-components"; import { ifProp } from "styled-tools"; import getConfig from "next/config"; import QRCode from "qrcode.react"; import Link from "next/link"; import differenceInMilliseconds from "date-fns/differenceInMilliseconds"; import ms from "ms"; import { removeProtocol, withComma, errorMessage } from "../utils"; import { useStoreActions, useStoreState } from "../store"; import { Link as LinkType } from "../store/links"; import { Checkbox, TextInput } from "./Input"; import { NavButton, Button } from "./Button"; import { Col, RowCenter } from "./Layout"; import Text, { H2, Span } from "./Text"; import { useMessage } from "../hooks"; import Animation from "./Animation"; import { Colors } from "../consts"; import Tooltip from "./Tooltip"; import Table from "./Table"; import ALink from "./ALink"; import Modal from "./Modal"; import Icon from "./Icon"; const { publicRuntimeConfig } = getConfig(); const Tr = styled(Flex).attrs({ as: "tr", px: [12, 12, 2] })``; const Th = styled(Flex)``; Th.defaultProps = { as: "th", flexBasis: 0, py: [12, 12, 3], px: [12, 12, 3] }; const Td = styled(Flex)<{ withFade?: boolean }>` position: relative; white-space: nowrap; ${ifProp( "withFade", css` :after { content: ""; position: absolute; right: 0; top: 0; height: 100%; width: 16px; background: linear-gradient(to left, white, rgba(255, 255, 255, 0.001)); } tr:hover &:after { background: linear-gradient( to left, ${Colors.TableRowHover}, rgba(255, 255, 255, 0.001) ); } ` )} `; Td.defaultProps = { as: "td", fontSize: [15, 16], alignItems: "center", flexBasis: 0, py: [12, 12, 3], px: [12, 12, 3] }; const EditContent = styled(Col)` border-bottom: 1px solid ${Colors.TableRowHover}; background-color: #fafafa; `; const Action = (props: React.ComponentProps) => ( ); const ogLinkFlex = { flexGrow: [1, 3, 7], flexShrink: [1, 3, 7] }; const createdFlex = { flexGrow: [1, 1, 2.5], flexShrink: [1, 1, 2.5] }; const shortLinkFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] }; const viewsFlex = { flexGrow: [0.5, 0.5, 1], flexShrink: [0.5, 0.5, 1], justifyContent: "flex-end" }; const actionsFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] }; interface RowProps { index: number; link: LinkType; setDeleteModal: (number) => void; } interface BanForm { host: boolean; user: boolean; userLinks: boolean; domain: boolean; } interface EditForm { target: string; address: string; description?: string; expire_in?: string; password?: string; } const Row: FC = ({ index, link, setDeleteModal }) => { const isAdmin = useStoreState(s => s.auth.isAdmin); const ban = useStoreActions(s => s.links.ban); const edit = useStoreActions(s => s.links.edit); const [banFormState, { checkbox }] = useFormState(); const [editFormState, { text, label, password }] = useFormState( { target: link.target, address: link.address, description: link.description, expire_in: link.expire_in ? ms(differenceInMilliseconds(new Date(link.expire_in), new Date()), { long: true }) : "", password: "" }, { withIds: true } ); const [copied, setCopied] = useState(false); const [showEdit, setShowEdit] = useState(false); const [qrModal, setQRModal] = useState(false); const [banModal, setBanModal] = useState(false); const [banLoading, setBanLoading] = useState(false); const [banMessage, setBanMessage] = useMessage(); const [editLoading, setEditLoading] = useState(false); const [editMessage, setEditMessage] = useMessage(); const onCopy = () => { setCopied(true); setTimeout(() => { setCopied(false); }, 1500); }; const onBan = async () => { setBanLoading(true); try { const res = await ban({ id: link.id, ...banFormState.values }); setBanMessage(res.message, "green"); setTimeout(() => { setBanModal(false); }, 2000); } catch (err) { setBanMessage(errorMessage(err)); } setBanLoading(false); }; const onEdit = async () => { if (editLoading) return; setEditLoading(true); try { await edit({ id: link.id, ...editFormState.values }); setShowEdit(false); } catch (err) { setEditMessage(errorMessage(err)); } editFormState.setField("password", ""); setEditLoading(false); }; const toggleEdit = () => { setShowEdit(s => !s); if (showEdit) editFormState.reset(); setEditMessage(""); }; return ( <> {link.target} {link.description && ( {link.description} )} {formatDistanceToNow(new Date(link.created_at))} ago {link.expire_in && ( Expires in{" "} {ms( differenceInMilliseconds(new Date(link.expire_in), new Date()), { long: true } )} )} {copied ? ( ) : ( )} {removeProtocol(link.link)} {withComma(link.visit_count)} {link.password && ( <> Password protected )} {link.banned && ( <> Banned )} {link.visit_count > 0 && ( )} setQRModal(true)} /> {isAdmin && !link.banned && ( setBanModal(true)} /> )} setDeleteModal(index)} /> {showEdit && ( Target: {link.domain || publicRuntimeConfig.DEFAULT_DOMAIN}/ Password Description: Expire in: {editMessage.text && ( {editMessage.text} )} )} setQRModal(false)} > setBanModal(false)} > <>

Ban link?

Are you sure do you want to ban the link{" "} "{removeProtocol(link.link)}"? {banLoading ? ( <> ) : banMessage.text ? ( {banMessage.text} ) : ( <> )}
); }; interface Form { all: boolean; limit: string; skip: string; search: string; } const LinksTable: FC = () => { const isAdmin = useStoreState(s => s.auth.isAdmin); const links = useStoreState(s => s.links); const { get, remove } = useStoreActions(s => s.links); const [tableMessage, setTableMessage] = useState("No links to show."); const [deleteModal, setDeleteModal] = useState(-1); const [deleteLoading, setDeleteLoading] = useState(false); const [deleteMessage, setDeleteMessage] = useMessage(); const [formState, { label, checkbox, text }] = useFormState
( { skip: "0", limit: "10", all: false }, { withIds: true } ); const options = formState.values; const linkToDelete = links.items[deleteModal]; useEffect(() => { get(options).catch(err => setTableMessage(err?.response?.data?.error || "An error occurred.") ); }, [options.limit, options.skip, options.all]); const onSubmit = e => { e.preventDefault(); get(options); }; const onDelete = async () => { setDeleteLoading(true); try { await remove(linkToDelete.id); await get(options); setDeleteModal(-1); } catch (err) { setDeleteMessage(errorMessage(err)); } setDeleteLoading(false); }; const onNavChange = (nextPage: number) => () => { formState.setField("skip", (parseInt(options.skip) + nextPage).toString()); }; const Nav = ( {["10", "25", "50"].map(c => ( { formState.setField("limit", c); formState.setField("skip", "0"); }} > {c} ))} links.total } ml={12} px={2} > ); return (

Recent shortened links.

{Nav} {!links.items.length ? ( ) : ( <> {links.items.map((link, index) => ( ))} )} {Nav}
{isAdmin && ( )}
Original URL Created Short URL Views
{links.loading ? "Loading links..." : tableMessage}
-1} closeHandler={() => setDeleteModal(-1)} > {linkToDelete && ( <>

Delete link?

Are you sure do you want to delete the link{" "} "{removeProtocol(linkToDelete.link)}"? {deleteLoading ? ( <> ) : deleteMessage.text ? ( {deleteMessage.text} ) : ( <> )} )}
); }; export default LinksTable;