├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── static │ └── OpenGraphEphemeral.png ├── src ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── editor.tsx │ └── index.tsx └── styles │ └── globals.css ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenGraphEphemeral 2 | 3 | ## Ephemeral Notes 4 | 5 | To write is to think. It’s the greatest tool we have. 6 | 7 | But it’s easy to slip out of writing to think into thinking to write. This is a writing tool that forces you to think. 8 | 9 | Ephemeral is a disappearing notepad: where all of your notes slowly fade over time. 10 | 11 | All words take 60 seconds to fully disappear. If you want to keep something, feel free to write it again - the same way you would call a thought back from memory. 12 | 13 | There’s no formatting tools, no bold, no italics, nothing. How much time do we spending obsessing over the way things look, rather than the thinking itself? 14 | 15 | This tool is intended to be used as you are deep in thought and want to boost your working memory, need to sketch something out when you can’t picture it in your head, or need to channel your stream of consciousness. 16 | 17 | [Try it out yourself](https://ephemeral-notes.com/). 18 | 19 | ### Development 20 | 21 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 22 | 23 | Run the development server: 24 | 25 | ```bash 26 | npm run dev 27 | # or 28 | yarn dev 29 | # or 30 | pnpm dev 31 | ``` 32 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ephemeral-notes", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "20.3.2", 13 | "@types/react": "18.2.14", 14 | "@types/react-dom": "18.2.6", 15 | "@vercel/analytics": "^1.0.1", 16 | "autoprefixer": "10.4.14", 17 | "draft-js": "^0.11.7", 18 | "eslint": "8.43.0", 19 | "eslint-config-next": "13.4.7", 20 | "next": "13.4.7", 21 | "postcss": "8.4.24", 22 | "postcss-nested": "^6.0.1", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "tailwindcss": "3.3.2", 26 | "typescript": "5.1.6" 27 | }, 28 | "devDependencies": { 29 | "@types/draft-js": "^0.11.12", 30 | "postcss-nesting": "^11.3.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss/nesting': 'postcss-nesting', 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0hq/ephemeral-notes/da407a04be762fd45e0efc5820e7235bb836b04a/public/favicon.ico -------------------------------------------------------------------------------- /public/static/OpenGraphEphemeral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0hq/ephemeral-notes/da407a04be762fd45e0efc5820e7235bb836b04a/public/static/OpenGraphEphemeral.png -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import { Analytics } from '@vercel/analytics/react'; 3 | import type { AppProps } from 'next/app'; 4 | import Head from 'next/head'; 5 | 6 | import { JetBrains_Mono } from 'next/font/google'; 7 | 8 | const jetbrains = JetBrains_Mono({ subsets: ['latin'] }) 9 | 10 | export default function App({ Component, pageProps }: AppProps) { 11 | return ( 12 | <> 13 | 14 | Ephemeral Notes 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 | 36 | ) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/editor.tsx: -------------------------------------------------------------------------------- 1 | import { CharacterMetadata, CompositeDecorator, ContentBlock, ContentState, Editor, EditorState } from 'draft-js'; 2 | import 'draft-js/dist/Draft.css'; 3 | import Immutable from 'immutable'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | const timeout = 120000 7 | 8 | function FadingSpan(props: any) { 9 | const [style, setStyle] = useState({ 10 | display: 'inline-block', 11 | transition: `opacity ${timeout / 1000}s, textSize ${timeout / 1000}s`, 12 | textSize: 'auto', // Start at normal height 13 | }); 14 | 15 | useEffect(() => { 16 | setStyle({ 17 | ...style, 18 | opacity: 0, 19 | textSize: 0, 20 | }); 21 | }, []); 22 | 23 | return {props.children}; 24 | } 25 | 26 | 27 | const decorator = new CompositeDecorator([ 28 | { 29 | strategy: (contentBlock, callback, contentState) => { 30 | const text = contentBlock.getText(); 31 | // split the text on spaces to find words 32 | const words = text.split(' '); 33 | let length = 0 34 | for (let i = 0; i < words.length; i++) { 35 | callback(length, length + words[i].length); 36 | length += words[i].length + 1 37 | } 38 | }, 39 | component: FadingSpan, 40 | }, 41 | ]); 42 | 43 | 44 | export default function Home() { 45 | const [blocks, setBlocks] = useState(new Map()); 46 | const [editorState, setEditorState] = useState(() => EditorState.createEmpty(decorator)); 47 | 48 | useEffect(() => { 49 | const timer = setInterval(() => { 50 | const currentTime = new Date().getTime(); 51 | const newBlocks = new Map(blocks); 52 | let shouldUpdate = false; 53 | 54 | newBlocks.forEach((value, key) => { 55 | const [text, timestamp] = value; 56 | 57 | if (currentTime - timestamp >= timeout) { 58 | newBlocks.set(key, ['', timestamp]); 59 | shouldUpdate = true; 60 | } 61 | }); 62 | 63 | if (shouldUpdate) { 64 | const newContentState = ContentState.createFromBlockArray( 65 | Array.from(newBlocks, ([key, [text]]) => new ContentBlock({ 66 | key: key, 67 | type: 'unstyled', 68 | text: text, 69 | characterList: Immutable.List( 70 | Array(text.length).fill( 71 | CharacterMetadata.create(), 72 | ), 73 | ), 74 | })), 75 | ); 76 | const newEditorState = EditorState.push(editorState, newContentState, 'change-block-data'); 77 | setEditorState(newEditorState); 78 | setBlocks(newBlocks); 79 | } 80 | }, 1000); 81 | 82 | return () => clearInterval(timer); 83 | }, [blocks, editorState]); 84 | 85 | const handleEditorChange = (newEditorState: EditorState) => { 86 | const newBlocks = new Map(); 87 | const currentTime = new Date().getTime(); 88 | 89 | newEditorState.getCurrentContent().getBlocksAsArray().forEach(block => { 90 | const oldBlockValue = blocks.get(block.getKey()); 91 | const newText = block.getText(); 92 | 93 | if (oldBlockValue) { 94 | const [oldText] = oldBlockValue; 95 | 96 | if (oldText === newText) { 97 | newBlocks.set(block.getKey(), oldBlockValue); 98 | } else { 99 | newBlocks.set(block.getKey(), [newText, currentTime]); 100 | } 101 | } else { 102 | newBlocks.set(block.getKey(), [newText, currentTime]); 103 | } 104 | }); 105 | 106 | setBlocks(newBlocks); 107 | setEditorState(newEditorState); 108 | }; 109 | 110 | 111 | return ( 112 |
113 |
114 |
115 |

Ephemeral Notes

116 |

60s

117 |
118 | 121 |
122 |
123 | 124 | ); 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useRouter } from "next/router"; 3 | import { useState } from "react"; 4 | 5 | export default function Home() { 6 | const router = useRouter() 7 | 8 | const [text, setText] = useState( 9 | `This project came from a conversation I had with a friend about how to properly use written notes and Google Docs to amplify the thought process, and the ways in which the appearance of organization, clarity, and infinite space can deceive and hurt us. 10 | 11 | I’ve recently found myself benefitting dramatically by writing very large documents that expand through all possible thought for a given idea or large decision, yet still, somewhat scared of it. The degree to which writing things in a document in a ‘clean way’ seems to calm my feeling of confusion, ambiguity, or doubt is frightening: is this clarity or only the feeling of it? 12 | 13 | There are many times in which sitting down, closing your eyes, and thinking about the problem in your own mind is extremely important. I’ve found myself occasionally going to the Archimedes Banya (of which I would strongly recommend) and sitting for hours in the various steam rooms and pools thinking about important questions. 14 | 15 | While indubitably productive, I found myself frustrated with the amount of “RAM” I had in my head, feeling like I couldn’t process the thoughts that weren’t purely subconscious, finding myself repeating things in my head and attempting to almost auto-regressively process them (and feeling like I was just a big organic language model). 16 | 17 | This is an experiment in trying to provide the great benefit of mental tools like typing out thoughts or sketching on a whiteboard, without offering the often misleading nature of such, whether infinitely branching different ideas without penalty, leaning on visual structure and organization as a substitute for true clarity, or the danger of accidentally only copying down instead of thinking.` 18 | ); 19 | const [pageState, setPageState] = useState(0); 20 | 21 | const updateState = () => { 22 | if (pageState == 0) { 23 | setPageState(1); 24 | setTimeout(() => { 25 | setText( 26 | `To write is to think. It’s the greatest tool we have. 27 | 28 | But it’s easy to slip out of writing to think into thinking to write. This is a writing tool that forces you to think. 29 | 30 | Ephemeral is a disappearing notepad: where all of your notes slowly fade over time. 31 | 32 | All words take 60 seconds to fully disappear. If you want to keep something, feel free to write it again - the same way you would call a thought back from memory. 33 | 34 | There’s no formatting tools, no bold, no italics, nothing. How much time do we spending obsessing over the way things look, rather than the thinking itself? 35 | 36 | This tool is intended to be used as you are deep in thought and want to boost your working memory, need to sketch something out when you can’t picture it in your head, or need to channel your stream of consciousness.` 37 | ); 38 | setPageState(2); 39 | }, 1500); // Adjust timing to match CSS transition 40 | } else { 41 | setPageState(3); 42 | setTimeout(() => { 43 | router.push("/editor"); 44 | }, 1500); // Adjust timing to match CSS transition 45 | } 46 | }; 47 | 48 | return ( 49 |
50 |
51 |
52 | {text.split("\n").map((line, i) =>

{line}

)} 53 | 56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | color: #fff; 7 | font-size: 14px; 8 | } 9 | 10 | @keyframes fadeOut { 11 | from { opacity: 1; } 12 | to { opacity: 0; } 13 | } 14 | 15 | /* .fade { 16 | animation-name: fadeOut; 17 | animation-duration: 2s; 18 | animation-fill-mode: forwards; 19 | } */ 20 | 21 | div.DraftEditor-root { 22 | border: 0px; 23 | background-color: white; 24 | color: #6A6A6A; 25 | font-size: 14px; 26 | height: 100%; 27 | width: 100%; 28 | overflow-y: auto; 29 | scrollbar-width: none; 30 | -ms-overflow-style: none; 31 | &::-webkit-scrollbar { 32 | width: 0px; 33 | background: transparent; /* make scrollbar transparent */ 34 | } 35 | border-top: #C9C9C9 2px solid; 36 | padding-top: 10px; 37 | transition: height 1s ease-in-out; /* Adjust duration and easing as needed */ 38 | } 39 | div.DraftEditor-editorContainer, 40 | div.public-DraftEditor-content { 41 | height: 100%; 42 | } 43 | 44 | .faded { 45 | height: 20px; /* Or whatever the height of your blocks are */ 46 | overflow: hidden; 47 | width: 0; 48 | } 49 | 50 | .DraftEditor-root { 51 | /* Other styles... */ 52 | transition: height 1s ease-in-out; /* Adjust duration and easing as needed */ 53 | } 54 | 55 | .fade-in { 56 | transition: opacity 1.5s ease-in-out; 57 | opacity: 1; 58 | } 59 | 60 | .fade-out { 61 | transition: opacity 1.5s ease-in-out; 62 | opacity: 0; 63 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx}', 5 | './src/components/**/*.{js,ts,jsx,tsx}', 6 | './src/app/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | duration: { 11 | '500': '500ms', 12 | }, 13 | transitionProperty: { 14 | 'height': 'height', 15 | 'opacity': 'opacity', 16 | } 17 | } 18 | }, 19 | plugins: [], 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | --------------------------------------------------------------------------------