Skip to content

Commit 2746469

Browse files
authored
feat: Implement share button #140 (#141)
1 parent 8f0204f commit 2746469

File tree

9 files changed

+181
-41
lines changed

9 files changed

+181
-41
lines changed

app/NoteDisplay.tsx

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { LANGUAGES, VALIDATION } from "./utils/constants";
1+
"use client";
2+
import { HOST } from "./utils/constants";
23
import dynamic from "next/dynamic";
34
import "@uiw/react-textarea-code-editor/dist.css";
45
import TextInput from "./TextInput";
56
import Truncate from "./Truncate";
67
import { BsFillTagFill } from "react-icons/bs";
7-
import { Fragment } from "react";
8+
import { Fragment, useState } from "react";
9+
import Button from "./Button";
10+
import { AiOutlineShareAlt } from "react-icons/ai";
11+
import SharePopup from "./SharePopup";
12+
import { getTagValues } from "./lib/utils";
13+
import { usePathname } from "next/navigation";
814

915
const CodeEditor = dynamic(
1016
() => import("@uiw/react-textarea-code-editor").then((mod) => mod.default),
1117
{ ssr: true }
1218
);
1319

1420
const NoteDisplay = ({ event }: any) => {
21+
const [isSharePopupOpen, setIsSharePopupOpen] = useState(false);
22+
const content = getTagValues("content", event.tags);
23+
const title = getTagValues("subject", event.tags);
24+
const pathname = usePathname();
1525
return (
1626
<Fragment>
1727
<div className="rounded-md border-2 border-secondary">
@@ -22,22 +32,25 @@ const NoteDisplay = ({ event }: any) => {
2232
type="text"
2333
list="filetypes"
2434
placeholder="filetype"
25-
// value={event.tags[0][1]}
26-
disabled={!!event}
27-
/>
28-
<datalist id="filetypes">
29-
{LANGUAGES.map((lang) => (
30-
<option key={lang} value={lang}>
31-
{lang}
32-
</option>
33-
))}
34-
</datalist>
35-
<Truncate
36-
content={event.content}
37-
iconOnly
38-
color="neutralLight"
39-
variant="ghost"
35+
value={getTagValues("filetype", event.tags)}
36+
disabled
4037
/>
38+
<div className="flex items-center gap-2">
39+
<Truncate
40+
content={event.content}
41+
iconOnly
42+
color="neutralLight"
43+
variant="ghost"
44+
title="Copy to clipboard"
45+
/>
46+
<Button
47+
color="neutralLight"
48+
variant="ghost"
49+
title="Share note"
50+
icon={<AiOutlineShareAlt />}
51+
onClick={() => setIsSharePopupOpen(true)}
52+
/>
53+
</div>
4154
</div>
4255
</div>
4356
<div className="flex h-[36rem] overflow-y-auto flex-col md:flex-row">
@@ -46,14 +59,10 @@ const NoteDisplay = ({ event }: any) => {
4659
required
4760
rows={1}
4861
className="bg-primary border-none focus:border-none resize-none font-medium text-2xl px-6 pt-6 pb-0 w-full overflow-hidden focus:ring-0"
49-
// title={event.tags[5][1]}
50-
// value={event.tags[5][1]}
62+
value={title}
5163
placeholder="Title..."
5264
disabled
5365
/>
54-
<span className="px-6 pt-0.5 text-xs text-red-500 hidden">
55-
{VALIDATION.required}
56-
</span>
5766
<div className="grow">
5867
<CodeEditor
5968
className={`w-full focus:border focus:border-blue-500 p-3 outline-none min-h-full "note-cursor-text"`}
@@ -73,11 +82,16 @@ const NoteDisplay = ({ event }: any) => {
7382
<div className="rounded-b-md border-x-2 border-b-2 border-secondary p-1 pt-2 -mt-1 flex items-center justify-between gap-4">
7483
<TextInput
7584
icon={<BsFillTagFill className="w-4 h-4" />}
76-
placeholder=""
77-
// tagsList={event?.tags[4][1].split(",")}
85+
tagsList={getTagValues("tags", event.tags).split(",")}
7886
disabled
7987
/>
8088
</div>
89+
<SharePopup
90+
link={`${HOST}${pathname}`}
91+
title="Share to"
92+
isOpen={isSharePopupOpen}
93+
setIsOpen={setIsSharePopupOpen}
94+
/>
8195
</Fragment>
8296
);
8397
};

app/Popup.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Fragment, ReactNode, useEffect } from "react";
22
import { IoMdCloseCircleOutline } from "react-icons/io";
33
import Button from "./Button";
44

5-
interface PopupProps {
5+
export interface PopupProps {
66
title: string;
77
isOpen: boolean;
88
setIsOpen: (isOpen: boolean) => void;
@@ -36,7 +36,7 @@ const Popup = ({
3636
return (
3737
<Fragment>
3838
<div
39-
className={`z-50 fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-[32rem] border-2 border-tertiary rounded-md overflow-hidden ${className}`}
39+
className={`z-50 fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-[33rem] border-2 border-tertiary rounded-md overflow-hidden ${className}`}
4040
>
4141
<Button
4242
icon={<IoMdCloseCircleOutline size={24} />}

app/SharePopup.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { AnchorHTMLAttributes, DetailedHTMLProps, FC } from "react";
2+
import { IconType } from "react-icons";
3+
import { AiOutlineTwitter, AiFillLinkedin } from "react-icons/ai";
4+
import { FaRedditAlien, FaTelegramPlane } from "react-icons/fa";
5+
import { MdEmail } from "react-icons/md";
6+
import { RiWhatsappFill } from "react-icons/ri";
7+
import useHover from "./hooks/useHover";
8+
import Popup, { PopupProps } from "./Popup";
9+
import Truncate from "./Truncate";
10+
11+
interface SharePopupProps extends PopupProps {
12+
link: string;
13+
}
14+
15+
const MESSAGE = "Checkout this note on notebin";
16+
const SOCLIAL_LINKS = [
17+
{
18+
label: "Twitter",
19+
url: (link: string) =>
20+
`https://twitter.com/intent/tweet?text=${MESSAGE}&url=${link}`,
21+
Icon: AiOutlineTwitter,
22+
color: "#1DA1F2",
23+
},
24+
{
25+
label: "LinkedIn",
26+
Icon: AiFillLinkedin,
27+
color: "#0077B5",
28+
url: (link: string) =>
29+
`https://www.linkedin.com/shareArticle?url=${link}&title=${MESSAGE}&mini=true`,
30+
},
31+
{
32+
label: "Reddit",
33+
url: (link: string) =>
34+
`https://www.reddit.com/submit?url=${link}&title=${MESSAGE}`,
35+
Icon: FaRedditAlien,
36+
color: "#FF4500",
37+
},
38+
{
39+
label: "WhatsApp",
40+
url: (link: string) => `https://wa.me/?text=${MESSAGE} ${link}`,
41+
Icon: RiWhatsappFill,
42+
color: "#25D366",
43+
},
44+
{
45+
label: "Telegram",
46+
url: (link: string) => `https://t.me/share/url?url=${link}&text=${MESSAGE}`,
47+
Icon: FaTelegramPlane,
48+
color: "#0088CC",
49+
},
50+
{
51+
label: "Email",
52+
url: (link: string) => `mailto:?subject=${MESSAGE}&body=${link}`,
53+
Icon: MdEmail,
54+
color: "#D44638",
55+
},
56+
];
57+
58+
const SharePopup: FC<SharePopupProps> = ({ link, ...props }) => {
59+
return (
60+
<Popup {...props}>
61+
<div className="flex gap-2 items-center flex-wrap ">
62+
{SOCLIAL_LINKS.map(({ label, url, color, Icon }, idx) => (
63+
<SocialLink
64+
key={idx}
65+
href={url(link)}
66+
Icon={Icon}
67+
label={label}
68+
color={color}
69+
/>
70+
))}
71+
</div>
72+
<div className="flex items-center justify-center bg-secondary p-2 px-4 rounded-md">
73+
<Truncate content={link} length={20} color="transparent" />
74+
</div>
75+
</Popup>
76+
);
77+
};
78+
79+
interface SocialLinkProps
80+
extends DetailedHTMLProps<
81+
AnchorHTMLAttributes<HTMLAnchorElement>,
82+
HTMLAnchorElement
83+
> {
84+
label?: string;
85+
Icon: IconType;
86+
color: string;
87+
}
88+
89+
const SocialLink: FC<SocialLinkProps> = ({ label, color, Icon, ...props }) => {
90+
return (
91+
<a
92+
target="_blank"
93+
rel="noopener noreferrer"
94+
className="flex item-center justify-center gap-2 text-sm font-bold p-2 bg-secondary rounded-md flex-1 hover:shadow-accent hover:scale-101 hover:shadow-sm transition-colors"
95+
{...props}
96+
{...useHover({ color })}
97+
>
98+
<span>
99+
<Icon className="w-6 h-6" />
100+
</span>
101+
<span>{label}</span>
102+
</a>
103+
);
104+
};
105+
106+
export default SharePopup;

app/TextInput.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ interface TextInputProps
1111
icon?: JSX.Element;
1212
error?: string;
1313
tagsList?: string[];
14-
// TODO: Figure out what type this should be
15-
setTagsList?: any;
14+
setTagsList?: (tagsList: string[]) => void;
1615
value?: string;
1716
}
1817

@@ -60,7 +59,7 @@ const TextInput = ({
6059
props.disabled ? "" : "group-hover:block"
6160
}`}
6261
onClick={() =>
63-
setTagsList(
62+
setTagsList!(
6463
tagsList.filter((tagInList) => tagInList !== tag)
6564
)
6665
}

app/Truncate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface ITruncateProps extends IButtonProps {
1818

1919
const Truncate = ({
2020
content,
21-
length = 4,
21+
length,
2222
iconOnly = false,
2323
...props
2424
}: ITruncateProps) => {
@@ -35,7 +35,7 @@ const Truncate = ({
3535
? "Copied!"
3636
: isError
3737
? "Error"
38-
: shortenHash(content)}
38+
: shortenHash(content, length)}
3939
</span>
4040
<Button
4141
className={color}

app/hooks/useHover.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { CSSProperties, useState } from "react";
2+
3+
const useHover = (
4+
styleOnHover: CSSProperties,
5+
styleOnNotHover: CSSProperties = {}
6+
) => {
7+
const [style, setStyle] = useState(styleOnNotHover);
8+
9+
const onMouseEnter = () => setStyle(styleOnHover);
10+
const onMouseLeave = () => setStyle(styleOnNotHover);
11+
12+
return { style, onMouseEnter, onMouseLeave };
13+
};
14+
15+
export default useHover;

app/lib/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
export const shortenHash = (hash: string) => {
1+
export const shortenHash = (hash: string, length = 4 as number) => {
22
if (hash) {
3-
return hash.substring(0, 4) + "..." + hash.substring(hash.length - 4);
3+
return (
4+
hash.substring(0, length) + "..." + hash.substring(hash.length - length)
5+
);
46
}
57
};
8+
9+
export const getTagValues = (name: string, tags: string[][]) => {
10+
const [itemTag] = tags.filter((tag: string[]) => tag[0] === name);
11+
const [, item] = itemTag || [, undefined];
12+
return item;
13+
};

app/u/[npub]/Card.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { FaCalendarAlt } from "react-icons/fa";
1313
import { DUMMY_PROFILE_API } from "../../utils/constants";
1414
import { shortenHash } from "../../lib/utils";
1515
import { PostDirContext } from "../../context/post-dir-provider";
16+
import { getTagValues } from "../../lib/utils";
1617

1718
interface NoteProps
1819
extends DetailedHTMLProps<LiHTMLAttributes<HTMLLIElement>, HTMLLIElement> {
@@ -29,25 +30,20 @@ const Card: FC<NoteProps> = ({
2930
}) => {
3031
const { tags, content, created_at: createdAt, id: noteId } = event;
3132
const { isCol } = useContext(PostDirContext);
32-
const getValues = (name: string) => {
33-
const [itemTag] = tags.filter((tag: string[]) => tag[0] === name);
34-
const [, item] = itemTag || [, undefined];
35-
return item;
36-
};
3733

3834
const { data } = useProfile({
3935
pubkey: event.pubkey,
4036
});
4137

4238
const npub = nip19.npubEncode(event.pubkey);
4339

44-
// let actualTags: any = getValues("tags");
40+
// let actualTags: any = getTagValues("tags");
4541

4642
// if (actualTags) {
4743
// actualTags = actualTags.split(",");
4844
// }
4945

50-
const title = getValues("subject");
46+
const title = getTagValues("subject", tags);
5147
const markdownImageContent =
5248
/!\[[^\]]*\]\((?<filename>.*?)(?=\"|\))(?<title>\".*\")?\)/g.exec(content);
5349

@@ -94,7 +90,7 @@ const Card: FC<NoteProps> = ({
9490
</div>
9591
) : null}
9692
<DatePosted dateOnly={dateOnly} timestamp={createdAt} />
97-
<FileType type={getValues("filetype")} />
93+
<FileType type={getTagValues("filetype", tags)} />
9894
{/* {actualTags.length > 1 ? <NoteTags tags={actualTags} /> : null} */}
9995
</div>
10096
<div className="flex flex-col sm:flex-row gap-5 opacity-70">

app/utils/constants.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export const HOST = "https://notebin.org";
2+
13
export const RELAYS = [
24
"wss://nostr-pub.wellorder.net",
35
"wss://relay.nostr.ch",

0 commit comments

Comments
 (0)