├── notes └── .gitkeep ├── fs ├── index.js ├── promises.js └── package.json ├── src ├── config.js ├── db.server.js ├── LocationContext.client.js ├── NotePreview.js ├── Spinner.js ├── index.client.js ├── TextWithMarkdown.js ├── NoteListSkeleton.js ├── EditButton.client.js ├── SidebarNote.js ├── Cache.client.js ├── Root.client.js ├── SearchField.client.js ├── NoteList.server.js ├── Note.server.js ├── SidebarNote.client.js ├── NoteSkeleton.js ├── App.server.js └── NoteEditor.client.js ├── .funcignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── public ├── favicon.ico ├── chevron-down.svg ├── chevron-up.svg ├── cross.svg ├── checkmark.svg ├── logo.svg ├── index.html └── style.css ├── local.settings.json ├── host.json ├── scripts ├── init_db.sh ├── seed.js └── build.js ├── credentials.js ├── .prettierignore ├── .prettierrc.js ├── function_react ├── function.json └── index.server.js ├── function_notes_post └── function.json ├── function_notes_put └── function.json ├── .gitignore ├── function_notes_delete └── function.json ├── funcutil ├── auth.js ├── babelregister.server.js └── react-utils.server.js ├── LICENSE ├── .github └── workflows │ └── azure-static-web-apps-kind-wave-0f8b93b1e.yml ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── README.orig.md /notes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fs'); -------------------------------------------------------------------------------- /fs/promises.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fs').promises; -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apiBaseUrl: '/api' 3 | }; -------------------------------------------------------------------------------- /.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonychu/azure-functions-reactjs-server-components-demo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.postDeployTask": "npm install", 4 | "azureFunctions.projectLanguage": "JavaScript", 5 | "azureFunctions.projectRuntime": "~3", 6 | "debug.internalConsoleOptions": "neverOpen", 7 | "azureFunctions.preDeployTask": "npm prune" 8 | } -------------------------------------------------------------------------------- /local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node", 5 | "AzureWebJobsStorage": "", 6 | "languageWorkers__node__arguments": "--conditions=react-server", 7 | "BABEL_DISABLE_CACHE": "1", 8 | "NODE_ENV": "production" 9 | }, 10 | "Host": { 11 | "CORS": "*" 12 | } 13 | } -------------------------------------------------------------------------------- /host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[1.*, 2.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/init_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 5 | DROP TABLE IF EXISTS notes; 6 | CREATE TABLE notes ( 7 | id SERIAL PRIMARY KEY, 8 | created_at TIMESTAMP NOT NULL, 9 | updated_at TIMESTAMP NOT NULL, 10 | title TEXT, 11 | body TEXT 12 | ); 13 | EOSQL 14 | -------------------------------------------------------------------------------- /credentials.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | host: process.env.DB_HOST || 'localhost', 3 | database: 'notesapi', 4 | user: process.env.DB_USER || 'notesadmin', 5 | password: process.env.DB_PASSWORD || 'password', 6 | port: '5432', 7 | }; 8 | 9 | if (process.env.DB_SSL) { 10 | config.ssl = { 11 | rejectUnauthorized: false, 12 | }; 13 | } 14 | 15 | module.exports = config; -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .eslintcache 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.html 26 | *.json 27 | *.md 28 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | React Logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/db.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {Pool} from 'react-pg'; 10 | import credentials from '../credentials'; 11 | 12 | // Don't keep credentials in the source tree in a real app! 13 | export const db = new Pool(credentials); 14 | -------------------------------------------------------------------------------- /src/LocationContext.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {createContext, useContext} from 'react'; 10 | 11 | export const LocationContext = createContext(); 12 | export function useLocation() { 13 | return useContext(LocationContext); 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = { 12 | arrowParens: 'always', 13 | bracketSpacing: false, 14 | singleQuote: true, 15 | jsxBracketSameLine: true, 16 | trailingComma: 'es5', 17 | printWidth: 80, 18 | }; 19 | -------------------------------------------------------------------------------- /src/NotePreview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import TextWithMarkdown from './TextWithMarkdown'; 10 | 11 | export default function NotePreview({body}) { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /function_react/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "index.server.js", 3 | "entryPoint": "reactFunction", 4 | "bindings": [ 5 | { 6 | "authLevel": "anonymous", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req", 10 | "methods": [ 11 | "get", 12 | "post" 13 | ], 14 | "route": "react" 15 | }, 16 | { 17 | "type": "http", 18 | "direction": "out", 19 | "name": "$return" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/Spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | export default function Spinner({active = true}) { 10 | return ( 11 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {unstable_createRoot} from 'react-dom'; 10 | import Root from './Root.client'; 11 | 12 | const initialCache = new Map(); 13 | const root = unstable_createRoot(document.getElementById('root')); 14 | root.render(); 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-node-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm install" 10 | }, 11 | { 12 | "type": "shell", 13 | "label": "npm install", 14 | "command": "npm install" 15 | }, 16 | { 17 | "type": "shell", 18 | "label": "npm prune", 19 | "command": "npm prune --production", 20 | "problemMatcher": [] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /function_notes_post/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "../function_react/index.server.js", 3 | "entryPoint": "notesPostFunction", 4 | "bindings": [ 5 | { 6 | "authLevel": "anonymous", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req", 10 | "methods": [ 11 | "post" 12 | ], 13 | "route": "notes" 14 | }, 15 | { 16 | "type": "http", 17 | "direction": "out", 18 | "name": "$return" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /function_notes_put/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "../function_react/index.server.js", 3 | "entryPoint": "notesPutFunction", 4 | "bindings": [ 5 | { 6 | "authLevel": "anonymous", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req", 10 | "methods": [ 11 | "put" 12 | ], 13 | "route": "notes/{id}" 14 | }, 15 | { 16 | "type": "http", 17 | "direction": "out", 18 | "name": "$return" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | /dist 14 | 15 | # notes 16 | notes/*.md 17 | 18 | # misc 19 | .DS_Store 20 | .eslintcache 21 | .env 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # vscode 32 | -------------------------------------------------------------------------------- /function_notes_delete/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "../function_react/index.server.js", 3 | "entryPoint": "notesDeleteFunction", 4 | "bindings": [ 5 | { 6 | "authLevel": "anonymous", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req", 10 | "methods": [ 11 | "delete" 12 | ], 13 | "route": "notes/{id}" 14 | }, 15 | { 16 | "type": "http", 17 | "direction": "out", 18 | "name": "$return" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/TextWithMarkdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import marked from 'marked'; 10 | import sanitizeHtml from 'sanitize-html'; 11 | 12 | const allowedTags = sanitizeHtml.defaults.allowedTags.concat([ 13 | 'img', 14 | 'h1', 15 | 'h2', 16 | 'h3', 17 | ]); 18 | const allowedAttributes = Object.assign( 19 | {}, 20 | sanitizeHtml.defaults.allowedAttributes, 21 | { 22 | img: ['alt', 'src'], 23 | } 24 | ); 25 | 26 | export default function TextWithMarkdown({text}) { 27 | return ( 28 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /funcutil/auth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // https://github.com/anthonychu/swa-api/blob/main/dist/auth.js 4 | 5 | 6 | function decodeAuthInfo(req) { 7 | if (!req) 8 | return; 9 | // This block sets a development user that has rights to upload 10 | // TODO: find a better way to do this 11 | if (process.env.FUNCTIONS_CORETOOLS_ENVIRONMENT) { 12 | return { 13 | identityProvider: "github", 14 | userId: "17baeed9bn1sa3e5dbs24283", 15 | userDetails: "testuser", 16 | userRoles: ["admin", "anonymous", "authenticated"], 17 | }; 18 | } 19 | var clientPrincipalHeader = "x-ms-client-principal"; 20 | if (req.headers[clientPrincipalHeader] == null) { 21 | return; 22 | } 23 | var buffer = Buffer.from(req.headers[clientPrincipalHeader], "base64"); 24 | var serializedJson = buffer.toString("ascii"); 25 | return JSON.parse(serializedJson); 26 | } 27 | 28 | module.exports = { 29 | decodeAuthInfo 30 | }; -------------------------------------------------------------------------------- /src/NoteListSkeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | export default function NoteListSkeleton() { 10 | return ( 11 |
12 |
    13 |
  • 14 |
    18 |
  • 19 |
  • 20 |
    24 |
  • 25 |
  • 26 |
    30 |
  • 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/EditButton.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {unstable_useTransition} from 'react'; 10 | 11 | import {useLocation} from './LocationContext.client'; 12 | 13 | export default function EditButton({noteId, children}) { 14 | const [, setLocation] = useLocation(); 15 | const [startTransition, isPending] = unstable_useTransition(); 16 | const isDraft = noteId == null; 17 | return ( 18 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/SidebarNote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {format, isToday} from 'date-fns'; 10 | import excerpts from 'excerpts'; 11 | import marked from 'marked'; 12 | 13 | import ClientSidebarNote from './SidebarNote.client'; 14 | 15 | export default function SidebarNote({note}) { 16 | const updatedAt = new Date(note.updated_at); 17 | const lastUpdatedAt = isToday(updatedAt) 18 | ? format(updatedAt, 'h:mm bb') 19 | : format(updatedAt, 'M/d/yy'); 20 | const summary = excerpts(marked(note.body), {words: 20}); 21 | return ( 22 | {summary || (No content)}

27 | }> 28 |
29 | {note.title} 30 | {lastUpdatedAt} 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Cache.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {unstable_getCacheForType, unstable_useCacheRefresh} from 'react'; 10 | import {createFromFetch} from 'react-server-dom-webpack'; 11 | import { apiBaseUrl } from './config'; 12 | 13 | function createResponseCache() { 14 | return new Map(); 15 | } 16 | 17 | export function useRefresh() { 18 | const refreshCache = unstable_useCacheRefresh(); 19 | return function refresh(key, seededResponse) { 20 | refreshCache(createResponseCache, new Map([[key, seededResponse]])); 21 | }; 22 | } 23 | 24 | export function useServerResponse(location) { 25 | const key = JSON.stringify(location); 26 | const cache = unstable_getCacheForType(createResponseCache); 27 | let response = cache.get(key); 28 | if (response) { 29 | return response; 30 | } 31 | const env = process.env.NODE_ENV; 32 | response = createFromFetch( 33 | fetch(apiBaseUrl + '/react?location=' + encodeURIComponent(key)) 34 | ); 35 | cache.set(key, response); 36 | return response; 37 | } 38 | -------------------------------------------------------------------------------- /funcutil/babelregister.server.js: -------------------------------------------------------------------------------- 1 | try { 2 | const register = require('react-server-dom-webpack/node-register'); 3 | register(); 4 | const babelRegister = require('@babel/register'); 5 | babelRegister({ 6 | ignore: [ 7 | // ignore build except if preceeded by react-static-web-apps-auth/ (reference to my local package) 8 | // ignore node_modules except if followed by react-static-web-apps-auth 9 | /[\\\/]((?>>>>> exiting\n\n\n"); 25 | process.exit(1); 26 | } 27 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Notes 9 | 10 | 11 |
12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Root.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {useState, Suspense} from 'react'; 10 | import {ErrorBoundary} from 'react-error-boundary'; 11 | 12 | import {useServerResponse} from './Cache.client'; 13 | import {LocationContext} from './LocationContext.client'; 14 | 15 | function reloadPage() { 16 | window.location.reload(); 17 | } 18 | 19 | export default function Root({initialCache}) { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | function Content() { 30 | const [location, setLocation] = useState({ 31 | selectedId: null, 32 | isEditing: false, 33 | searchText: '', 34 | }); 35 | const response = useServerResponse(location); 36 | return ( 37 | 38 | {response.readRoot()} 39 | 40 | ); 41 | } 42 | 43 | function Error({error}) { 44 | return ( 45 |
46 |
{error.stack}
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/SearchField.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {useState, unstable_useTransition} from 'react'; 10 | 11 | import {useLocation} from './LocationContext.client'; 12 | import Spinner from './Spinner'; 13 | 14 | export default function SearchField() { 15 | const [text, setText] = useState(''); 16 | const [startSearching, isSearching] = unstable_useTransition(false); 17 | const [, setLocation] = useLocation(); 18 | return ( 19 |
e.preventDefault()}> 20 | 23 | { 28 | const newText = e.target.value; 29 | setText(newText); 30 | startSearching(() => { 31 | setLocation((loc) => ({ 32 | ...loc, 33 | searchText: newText, 34 | })); 35 | }); 36 | }} 37 | /> 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/NoteList.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {fetch} from 'react-fetch'; 10 | 11 | import {db} from './db.server'; 12 | import SidebarNote from './SidebarNote'; 13 | 14 | export default function NoteList({ searchText, userInfo }) { 15 | // const notes = fetch('http://localhost:4000/notes').json(); 16 | 17 | // WARNING: This is for demo purposes only. 18 | // We don't encourage this in real apps. There are far safer ways to access 19 | // data in a real application! 20 | let notes = []; 21 | if (userInfo) { 22 | notes = db.query( 23 | `select * from notes where title ilike $1 and userid = $2 order by id desc`, 24 | ['%' + searchText + '%', userInfo.userId] 25 | ).rows; 26 | } 27 | 28 | // const notes = []; 29 | 30 | // Now let's see how the Suspense boundary above lets us not block on this. 31 | // fetch('http://localhost:4000/sleep/3000'); 32 | 33 | return notes.length > 0 ? ( 34 |
    35 | {notes.map((note) => ( 36 |
  • 37 | 38 |
  • 39 | ))} 40 |
41 | ) : ( 42 |
43 | {searchText 44 | ? `Couldn't find any notes titled "${searchText}".` 45 | : 'No notes created yet!'}{' '} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-kind-wave-0f8b93b1e.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - azure-static-web-apps 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - azure-static-web-apps 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Build app and API 22 | run: | # build and then remove package.json (so deploy step doesn't reinstall modules) 23 | npm install 24 | npm run build 25 | npm prune --production 26 | rm package.json package-lock.json 27 | ls -la build 28 | - name: Deploy 29 | id: builddeploy 30 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 31 | with: 32 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_WAVE_0F8B93B1E }} 33 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 34 | action: "upload" 35 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ###### 36 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 37 | app_location: "build" # App source code path 38 | api_location: "/" # Api source code path - optional 39 | output_location: "" # Built app content directory - optional 40 | ###### End of Repository/Build Configurations ###### 41 | 42 | close_pull_request_job: 43 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 44 | runs-on: ubuntu-latest 45 | name: Close Pull Request Job 46 | steps: 47 | - name: Close Pull Request 48 | id: closepullrequest 49 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 50 | with: 51 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_WAVE_0F8B93B1E }} 52 | action: "close" 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-notes", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | }, 7 | "license": "MIT", 8 | "dependencies": { 9 | "@aaronpowell/react-static-web-apps-auth": "git+https://github.com/anthonychu/react-static-web-apps-auth.git#react-server-components", 10 | "@babel/core": "7.12.3", 11 | "@babel/register": "^7.12.1", 12 | "babel-loader": "8.1.0", 13 | "babel-preset-react-app": "10.0.0", 14 | "compression": "^1.7.4", 15 | "date-fns": "^2.16.1", 16 | "excerpts": "^0.0.3", 17 | "fs": "file:fs", 18 | "html-webpack-plugin": "4.5.0", 19 | "marked": "^1.2.5", 20 | "memory-stream": "^1.0.0", 21 | "pg": "^8.5.1", 22 | "react": "0.0.0-experimental-3310209d0", 23 | "react-dom": "0.0.0-experimental-3310209d0", 24 | "react-error-boundary": "^3.1.0", 25 | "react-fetch": "0.0.0-experimental-3310209d0", 26 | "react-fs": "0.0.0-experimental-3310209d0", 27 | "react-pg": "0.0.0-experimental-3310209d0", 28 | "react-server-dom-webpack": "0.0.0-experimental-3310209d0", 29 | "resolve": "1.12.0", 30 | "rimraf": "^3.0.2", 31 | "sanitize-html": "^2.2.0", 32 | "webpack": "4.44.2" 33 | }, 34 | "devDependencies": { 35 | "cross-env": "^7.0.3", 36 | "dotenv": "^8.2.0", 37 | "filemanager-webpack-plugin": "^3.0.0", 38 | "prettier": "1.19.1", 39 | "webpack-cli": "^4.2.0" 40 | }, 41 | "scripts": { 42 | "start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"", 43 | "start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"", 44 | "server:dev": "cross-env NODE_ENV=development nodemon -- --conditions=react-server server", 45 | "server:prod": "cross-env NODE_ENV=production nodemon -- --conditions=react-server server", 46 | "bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js", 47 | "bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js", 48 | "prettier": "prettier --write **/*.js", 49 | "seed": "node ./scripts/seed.js", 50 | "build": "cross-env NODE_ENV=production node scripts/build.js" 51 | }, 52 | "babel": { 53 | "presets": [ 54 | [ 55 | "react-app", 56 | { 57 | "runtime": "automatic" 58 | } 59 | ] 60 | ] 61 | }, 62 | "nodemonConfig": { 63 | "ignore": [ 64 | "build/*" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Note.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { fetch } from 'react-fetch'; 10 | import { readFile } from 'react-fs'; 11 | import { format } from 'date-fns'; 12 | import path from 'path'; 13 | 14 | import { db } from './db.server'; 15 | import NotePreview from './NotePreview'; 16 | import EditButton from './EditButton.client'; 17 | import NoteEditor from './NoteEditor.client'; 18 | 19 | export default function Note({ selectedId, isEditing, userInfo }) { 20 | let note = null; 21 | 22 | if (selectedId !== null && userInfo) { 23 | const notes = db.query( 24 | 'select * from notes where id = $1 and userid = $2', 25 | [ selectedId, userInfo.userId ] 26 | ).rows; 27 | 28 | if (notes.length) { 29 | note = notes[0]; 30 | } 31 | } 32 | 33 | if (note === null) { 34 | if (isEditing) { 35 | return ( 36 | 37 | ); 38 | } else { 39 | return ( 40 |
41 | 42 | Click a note on the left to view something! 🥺 43 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | let { id, title, body, updated_at } = note; 50 | const updatedAt = new Date(updated_at); 51 | 52 | // We could also read from a file instead. 53 | // body = readFile(path.resolve(`./notes/${note.id}.md`), 'utf8'); 54 | 55 | // Now let's see how the Suspense boundary above lets us not block on this. 56 | // fetch('http://localhost:4000/sleep/3000'); 57 | 58 | if (isEditing) { 59 | return ; 60 | } else { 61 | return ( 62 |
63 |
64 |

{title}

65 |
66 | 67 | Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")} 68 | 69 | Edit 70 |
71 |
72 | 73 |
74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/SidebarNote.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {useState, useRef, useEffect, unstable_useTransition} from 'react'; 10 | 11 | import {useLocation} from './LocationContext.client'; 12 | 13 | export default function SidebarNote({id, title, children, expandedChildren}) { 14 | const [location, setLocation] = useLocation(); 15 | const [startTransition, isPending] = unstable_useTransition(); 16 | const [isExpanded, setIsExpanded] = useState(false); 17 | const isActive = id === location.selectedId; 18 | 19 | // Animate after title is edited. 20 | const itemRef = useRef(null); 21 | const prevTitleRef = useRef(title); 22 | useEffect(() => { 23 | if (title !== prevTitleRef.current) { 24 | prevTitleRef.current = title; 25 | itemRef.current.classList.add('flash'); 26 | } 27 | }, [title]); 28 | 29 | return ( 30 |
{ 33 | itemRef.current.classList.remove('flash'); 34 | }} 35 | className={[ 36 | 'sidebar-note-list-item', 37 | isExpanded ? 'note-expanded' : '', 38 | ].join(' ')}> 39 | {children} 40 | 63 | 80 | {isExpanded && expandedChildren} 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /funcutil/react-utils.server.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs'); 2 | const path = require('path'); 3 | const { pipeToNodeWritable } = require('react-server-dom-webpack/writer'); 4 | const MemoryStream = require('memory-stream'); 5 | 6 | let moduleMapCache = null; 7 | function getModuleMap() { 8 | if (moduleMapCache) { 9 | return moduleMapCache; 10 | } 11 | 12 | const manifest = readFileSync( 13 | path.resolve(__dirname, '../build/react-client-manifest.json'), 14 | 'utf8' 15 | ); 16 | const moduleMap = JSON.parse(manifest); 17 | 18 | moduleMapCache = new Proxy(moduleMap, { 19 | get: function (target, prop, receiver) { 20 | if (target[prop]) { 21 | return target[prop]; 22 | } 23 | 24 | const bestKey = findBestMatchedKey(target, prop); 25 | if (bestKey) { 26 | return target[bestKey]; 27 | } 28 | 29 | function findBestMatchedKey(target, prop) { 30 | const propChars = prop.split(''); 31 | const scoredKeys = Object.keys(target) 32 | .map(k => { 33 | // compare strings from end and return number of matching characters 34 | const keyChars = k.split(''); 35 | 36 | for (let i = 0; i < keyChars.length && i < propChars.length; i++) { 37 | if (keyChars[keyChars.length - i - 1] !== propChars[propChars.length - i - 1]) { 38 | return { 39 | key: k, 40 | matchedChars: i 41 | }; 42 | } 43 | } 44 | return { 45 | key: k, 46 | matchedChars: 0 47 | }; 48 | }) 49 | .sort((a, b) => b.matchedChars - a.matchedChars); 50 | 51 | if (scoredKeys.length && scoredKeys[0].matchedChars) { 52 | return scoredKeys[0].key; 53 | } 54 | } 55 | } 56 | }); 57 | return moduleMapCache; 58 | } 59 | 60 | function createResponseBody(reactElement) { 61 | return new Promise((resolve) => { 62 | const outputStream = new MemoryStream(); 63 | outputStream.on('finish', () => resolve(outputStream.toString())); 64 | pipeToNodeWritable(reactElement, outputStream, getModuleMap()); 65 | }); 66 | } 67 | 68 | module.exports = { 69 | getModuleMap, 70 | createResponseBody, 71 | }; -------------------------------------------------------------------------------- /src/NoteSkeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | function NoteEditorSkeleton() { 10 | return ( 11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ); 44 | } 45 | 46 | function NotePreviewSkeleton() { 47 | return ( 48 |
52 |
53 |
57 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ); 71 | } 72 | 73 | export default function NoteSkeleton({isEditing}) { 74 | return isEditing ? : ; 75 | } 76 | -------------------------------------------------------------------------------- /src/App.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { Suspense } from 'react'; 10 | 11 | import Note from './Note.server'; 12 | import NoteList from './NoteList.server'; 13 | import EditButton from './EditButton.client'; 14 | import SearchField from './SearchField.client'; 15 | import NoteSkeleton from './NoteSkeleton'; 16 | import NoteListSkeleton from './NoteListSkeleton'; 17 | 18 | import { Logout, StaticWebAuthLogins } from "@aaronpowell/react-static-web-apps-auth"; 19 | 20 | export default function App({ selectedId, isEditing, searchText, userInfo }) { 21 | 22 | return ( 23 |
24 |
25 |
26 | 34 | React Notes 35 |
36 | { userInfo && 37 | <> 38 |
39 | {userInfo.userDetails} | 40 |
41 |
42 | 43 | New 44 |
45 | 50 | 51 | } 52 |
53 | { 54 | userInfo 55 | ?
56 | }> 57 | 58 | 59 |
60 | :
61 |

62 | Welcome to the a demo of React Server Components running on Azure Static Web Apps. 63 | See the GitHub repo to learn more. 64 |
  65 |

66 |

67 | To view and edit notes, log in with one of these providers:
  68 |

69 | 70 |
71 | } 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /function_react/index.server.js: -------------------------------------------------------------------------------- 1 | require('../funcutil/babelregister.server'); 2 | 3 | const React = require('react'); 4 | const ReactApp = require('../src/App.server').default; 5 | const { Pool } = require('pg'); 6 | const pool = new Pool(require('../credentials')); 7 | 8 | const { createResponseBody } = require('../funcutil/react-utils.server'); 9 | 10 | const { decodeAuthInfo } = require('../funcutil/auth'); 11 | 12 | async function reactFunction(context, req) { 13 | return await createResponse(context, req); 14 | } 15 | 16 | async function notesPutFunction(context, req) { 17 | const userInfo = decodeAuthInfo(req); 18 | if (!userInfo) { 19 | return { status: 401 }; 20 | } 21 | 22 | const now = new Date(); 23 | const updatedId = Number(req.params.id); 24 | await pool.query( 25 | 'update notes set title = $1, body = $2, updated_at = $3 where id = $4 and userid = $5', 26 | [req.body.title, req.body.body, now, updatedId, userInfo.userId] 27 | ); 28 | return await createResponse(context, req); 29 | } 30 | 31 | async function notesPostFunction(context, req) { 32 | const userInfo = decodeAuthInfo(req); 33 | if (!userInfo) { 34 | return { status: 401 }; 35 | } 36 | 37 | const now = new Date(); 38 | const result = await pool.query( 39 | 'insert into notes (title, body, created_at, updated_at, userid) values ($1, $2, $3, $3, $4) returning id', 40 | [req.body.title, req.body.body, now, userInfo.userId] 41 | ); 42 | const insertedId = result.rows[0].id; 43 | return await createResponse(context, req, insertedId); 44 | } 45 | 46 | async function notesDeleteFunction(context, req) { 47 | const userInfo = decodeAuthInfo(req); 48 | if (!userInfo) { 49 | return { status: 401 }; 50 | } 51 | 52 | await pool.query( 53 | 'delete from notes where id = $1 and userid = $2', 54 | [req.params.id, userInfo.userId]); 55 | return await createResponse(context, req); 56 | } 57 | 58 | async function createResponse(context, req, redirectToId) { 59 | const location = JSON.parse(req.query.location); 60 | 61 | if (redirectToId) { 62 | location.selectedId = redirectToId; 63 | } 64 | 65 | const userInfo = decodeAuthInfo(req); 66 | 67 | const props = { 68 | userInfo, 69 | selectedId: location.selectedId, 70 | isEditing: location.isEditing, 71 | searchText: location.searchText, 72 | }; 73 | 74 | if (userInfo) { 75 | context.log(JSON.stringify(userInfo, null, 2)); 76 | } 77 | 78 | const responseBody = await createResponseBody(React.createElement(ReactApp, props)); 79 | return { 80 | body: responseBody, 81 | headers: { 82 | 'X-Location': JSON.stringify(location), 83 | 'Access-Control-Expose-Headers': 'X-Location' 84 | } 85 | }; 86 | } 87 | 88 | module.exports = { 89 | reactFunction, 90 | notesDeleteFunction, 91 | notesPostFunction, 92 | notesPutFunction, 93 | }; -------------------------------------------------------------------------------- /scripts/seed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const {Pool} = require('pg'); 14 | const {readdir, unlink, writeFile} = require('fs/promises'); 15 | const startOfYear = require('date-fns/startOfYear'); 16 | const credentials = require('../credentials'); 17 | 18 | const NOTES_PATH = './notes'; 19 | const pool = new Pool(credentials); 20 | 21 | const now = new Date(); 22 | const startOfThisYear = startOfYear(now); 23 | // Thanks, https://stackoverflow.com/a/9035732 24 | function randomDateBetween(start, end) { 25 | return new Date( 26 | start.getTime() + Math.random() * (end.getTime() - start.getTime()) 27 | ); 28 | } 29 | 30 | const dropTableStatement = 'DROP TABLE IF EXISTS notes;'; 31 | const createTableStatement = `CREATE TABLE notes ( 32 | id SERIAL PRIMARY KEY, 33 | created_at TIMESTAMP NOT NULL, 34 | updated_at TIMESTAMP NOT NULL, 35 | title TEXT, 36 | userid TEXT, 37 | body TEXT 38 | );`; 39 | const insertNoteStatement = `INSERT INTO notes(title, body, created_at, updated_at) 40 | VALUES ($1, $2, $3, $3) 41 | RETURNING *`; 42 | const seedData = [ 43 | [ 44 | 'Meeting Notes', 45 | 'This is an example note. It contains **Markdown**!', 46 | randomDateBetween(startOfThisYear, now), 47 | ], 48 | [ 49 | 'Make a thing', 50 | `It's very easy to make some words **bold** and other words *italic* with 51 | Markdown. You can even [link to React's website!](https://www.reactjs.org).`, 52 | randomDateBetween(startOfThisYear, now), 53 | ], 54 | [ 55 | 'A note with a very long title because sometimes you need more words', 56 | `You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing) 57 | notes in this app! These note live on the server in the \`notes\` folder. 58 | 59 | ![This app is powered by React](https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/React_Native_Logo.png/800px-React_Native_Logo.png)`, 60 | randomDateBetween(startOfThisYear, now), 61 | ], 62 | ['I wrote this note today', 'It was an excellent note.', now], 63 | ]; 64 | 65 | async function seed() { 66 | await pool.query(dropTableStatement); 67 | await pool.query(createTableStatement); 68 | const res = await Promise.all( 69 | seedData.map((row) => pool.query(insertNoteStatement, row)) 70 | ); 71 | console.log(res); 72 | 73 | // const oldNotes = await readdir(path.resolve(NOTES_PATH)); 74 | // await Promise.all( 75 | // oldNotes 76 | // .filter((filename) => filename.endsWith('.md')) 77 | // .map((filename) => unlink(path.resolve(NOTES_PATH, filename))) 78 | // ); 79 | 80 | // await Promise.all( 81 | // res.map(({rows}) => { 82 | // const id = rows[0].id; 83 | // const content = rows[0].body; 84 | // const data = new Uint8Array(Buffer.from(content)); 85 | // return writeFile(path.resolve(NOTES_PATH, `${id}.md`), data, (err) => { 86 | // if (err) { 87 | // throw err; 88 | // } 89 | // }); 90 | // }) 91 | // ); 92 | } 93 | 94 | seed(); 95 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | const rimraf = require('rimraf'); 13 | const webpack = require('webpack'); 14 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 15 | const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin'); 16 | const FileManagerPlugin = require('filemanager-webpack-plugin'); 17 | 18 | const isProduction = process.env.NODE_ENV === 'production'; 19 | rimraf.sync(path.resolve(__dirname, '../build')); 20 | webpack( 21 | { 22 | mode: isProduction ? 'production' : 'development', 23 | devtool: isProduction ? 'source-map' : 'cheap-module-source-map', 24 | entry: [path.resolve(__dirname, '../src/index.client.js')], 25 | output: { 26 | path: path.resolve(__dirname, '../build'), 27 | filename: 'main.js', 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.js$/, 33 | use: 'babel-loader', 34 | exclude: /node_modules/, 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | inject: true, 41 | template: path.resolve(__dirname, '../public/index.html'), 42 | }), 43 | new ReactServerWebpackPlugin({ isServer: false }), 44 | new FileManagerPlugin({ 45 | events: { 46 | // copy public folder over to build, but keep what's already in build 47 | onEnd: [ 48 | { 49 | mkdir: [path.resolve(__dirname, '../buildtemp')], 50 | }, 51 | { 52 | copy: [ 53 | { 54 | source: path.resolve(__dirname, '../public'), 55 | destination: path.resolve(__dirname, '../buildtemp') 56 | }, 57 | ] 58 | }, 59 | { 60 | copy: [ 61 | { 62 | source: path.resolve(__dirname, '../build'), 63 | destination: path.resolve(__dirname, '../buildtemp') 64 | }, 65 | ] 66 | }, 67 | { 68 | copy: [ 69 | { 70 | source: path.resolve(__dirname, '../buildtemp'), 71 | destination: path.resolve(__dirname, '../build') 72 | }, 73 | ] 74 | }, 75 | { 76 | delete: [path.resolve(__dirname, '../buildtemp')] 77 | }, 78 | ], 79 | }, 80 | }), 81 | ], 82 | }, 83 | (err, stats) => { 84 | if (err) { 85 | console.error(err.stack || err); 86 | if (err.details) { 87 | console.error(err.details); 88 | } 89 | process.exit(1); 90 | return; 91 | } 92 | const info = stats.toJson(); 93 | if (stats.hasErrors()) { 94 | console.log('Finished running webpack with errors.'); 95 | info.errors.forEach((e) => console.error(e)); 96 | process.exit(1); 97 | } else { 98 | console.log('Finished running webpack.'); 99 | } 100 | } 101 | ); 102 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/NoteEditor.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {useState, unstable_useTransition} from 'react'; 10 | import {createFromReadableStream} from 'react-server-dom-webpack'; 11 | import {apiBaseUrl} from './config'; 12 | 13 | import NotePreview from './NotePreview'; 14 | import {useRefresh} from './Cache.client'; 15 | import {useLocation} from './LocationContext.client'; 16 | 17 | export default function NoteEditor({noteId, initialTitle, initialBody}) { 18 | const refresh = useRefresh(); 19 | const [title, setTitle] = useState(initialTitle); 20 | const [body, setBody] = useState(initialBody); 21 | const [location, setLocation] = useLocation(); 22 | const [startNavigating, isNavigating] = unstable_useTransition(); 23 | const [isSaving, saveNote] = useMutation({ 24 | endpoint: noteId !== null ? `${apiBaseUrl}/notes/${noteId}` : `${apiBaseUrl}/notes`, 25 | method: noteId !== null ? 'PUT' : 'POST', 26 | }); 27 | const [isDeleting, deleteNote] = useMutation({ 28 | endpoint: `${apiBaseUrl}/notes/${noteId}`, 29 | method: 'DELETE', 30 | }); 31 | 32 | async function handleSave() { 33 | const payload = {title, body}; 34 | const requestedLocation = { 35 | selectedId: noteId, 36 | isEditing: false, 37 | searchText: location.searchText, 38 | }; 39 | const response = await saveNote(payload, requestedLocation); 40 | navigate(response); 41 | } 42 | 43 | async function handleDelete() { 44 | const payload = {}; 45 | const requestedLocation = { 46 | selectedId: null, 47 | isEditing: false, 48 | searchText: location.searchText, 49 | }; 50 | const response = await deleteNote(payload, requestedLocation); 51 | navigate(response); 52 | } 53 | 54 | function navigate(response) { 55 | let cacheKey = response.headers.get('X-Location'); 56 | let nextLocation = JSON.parse(cacheKey); 57 | const seededResponse = createFromReadableStream(response.body); 58 | startNavigating(() => { 59 | refresh(cacheKey, seededResponse); 60 | setLocation(nextLocation); 61 | }); 62 | } 63 | 64 | const isDraft = noteId === null; 65 | return ( 66 |
67 |
e.preventDefault()}> 71 | 74 | { 79 | setTitle(e.target.value); 80 | }} 81 | /> 82 | 85 |