├── .env.example ├── .gitignore ├── components ├── button.tsx ├── edit-link-modal.tsx ├── editor.tsx ├── github.tsx ├── input.tsx ├── save-bar.tsx ├── spinner.tsx └── top-bar.tsx ├── lib ├── data.ts └── db.ts ├── license.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── get-page.ts │ ├── save-page.ts │ └── send-email.ts └── index.tsx ├── public ├── emoji-bg.png ├── emoji-bg.svg ├── favicon.ico └── twitter-card.png ├── readme.md ├── sendgrid.json ├── tsconfig.json ├── views ├── editor.tsx ├── fixed-center.tsx ├── static-layout.tsx └── welcome.tsx └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | PUSHER_APP_SECRET= 2 | PUSHER_APP_ID= 3 | SENDGRID_STATIC_FUN_KEY= 4 | FAUNADB_STATIC_FUN_KEY= 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel -------------------------------------------------------------------------------- /components/button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Button({ 4 | children, 5 | fontFamily, 6 | bg, 7 | width, 8 | height, 9 | fontSize, 10 | isLoading, 11 | ...rest 12 | }: React.DetailedHTMLProps< 13 | React.ButtonHTMLAttributes, 14 | HTMLButtonElement 15 | > & { 16 | children: React.ReactNode; 17 | fontFamily?: string; 18 | fontSize?: number; 19 | width?: number; 20 | height?: number; 21 | isLoading?: boolean; 22 | bg?: string; 23 | }) { 24 | return ( 25 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/edit-link-modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | import Button from "./button"; 4 | 5 | export default function EditLinkModal({ 6 | dialogOpen, 7 | setDialogOpen, 8 | email, 9 | setEmail, 10 | editLink 11 | }) { 12 | const [sendingState, setSendingState] = useState(); 13 | const [errorMessage, setErrorMessage] = useState(); 14 | 15 | const dialogRef = useRef(); 16 | const editLinkRef = useRef(); 17 | 18 | useEffect(() => { 19 | import("dialog-polyfill").then(dp => { 20 | dp.default.registerDialog(dialogRef.current); 21 | }); 22 | if (dialogOpen) { 23 | dialogRef.current.showModal(); 24 | editLinkRef.current.focus(); 25 | editLinkRef.current.select(); 26 | document.execCommand("copy"); 27 | } 28 | }, [dialogOpen]); 29 | 30 | async function sendEmail() { 31 | console.log("sending email to: ", email); 32 | if (email) { 33 | setSendingState("SENDING"); 34 | setErrorMessage(null); 35 | let res = await fetch("/api/send-email", { 36 | method: "POST", 37 | headers: { 38 | "Content-Type": "application/json" 39 | }, 40 | body: JSON.stringify({ email, editLink }) 41 | }); 42 | if (res.ok) { 43 | setSendingState("SUCCESS"); 44 | localStorage.setItem("email", email); 45 | } 46 | if (!res.ok) { 47 | setSendingState("ERROR"); 48 | let { message } = await res.json(); 49 | setErrorMessage(message); 50 | } 51 | } 52 | } 53 | 54 | function emailInputHandler(e) { 55 | setEmail(e.target.value); 56 | } 57 | 58 | function renderEmailButton() { 59 | switch (sendingState) { 60 | case "SENDING": 61 | return ( 62 | 65 | ); 66 | case "ERROR": 67 | return ( 68 | 71 | ); 72 | case "SUCCESS": 73 | return ( 74 | 77 | ); 78 | default: 79 | return ( 80 | 88 | ); 89 | } 90 | } 91 | 92 | return ( 93 | 94 |
95 |
96 |

Secret Edit Link

97 | 98 |

99 | Please don't share the edit link with anyone you don't want editing 100 | your page! 101 |

102 |
103 |
104 |
105 |

Enter your email to save the edit link (recommended)

106 |
107 |
108 | 114 | {renderEmailButton()} 115 | {errorMessage &&

{errorMessage}

} 116 |
117 | {sendingState === "SUCCESS" && ( 118 |

119 | Email sent successfully! Please check your spam folder if you can't 120 | find it in your inbox yet 121 |

122 | )} 123 | 124 |

{ 127 | dialogRef.current.close(); 128 | setSendingState(null); 129 | setErrorMessage(null); 130 | setDialogOpen(false); 131 | }} 132 | > 133 | Close 134 |

135 |
136 | 240 |
241 | ); 242 | } 243 | -------------------------------------------------------------------------------- /components/editor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | import EditLinkModal from "./edit-link-modal"; 4 | import SaveBar from "./save-bar"; 5 | 6 | export default function EditorContainer({ html, email, editLink }) { 7 | const [_html, setHtml] = useState(html || ""); 8 | const [_email, setEmail] = useState(email); 9 | const [dialogOpen, setDialogOpen] = useState(false); 10 | 11 | return ( 12 |
13 | 20 |
21 | 27 |
28 |
29 | 30 |
31 | 32 | 69 |
70 | ); 71 | } 72 | 73 | function Editor({ html, email, setHtml, setDialogOpen }) { 74 | const [saveState, setSaveState] = useState(); 75 | const [showEditLink, setShowEditLink] = useState(false); 76 | 77 | function onChange(e) { 78 | setHtml(e.target.value); 79 | if (saveState === "SUCCESS") { 80 | setSaveState("DEFAULT"); 81 | } 82 | } 83 | return ( 84 |
85 | 93 |