├── .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 |
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 |
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 |
54 | {pageState > 1 ? 'Start' : 'Next'} ->
55 |
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 |
--------------------------------------------------------------------------------