├── .babelrc ├── LICENSE.md ├── README.md ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── package-lock.json ├── package.json ├── src ├── components │ ├── AppContext.js │ ├── BlockContextMenu.js │ ├── BlockEditorSlate.js │ ├── CapturedContentASResult.js │ ├── CardContextMenu.js │ ├── ClickHandler.js │ ├── CollectionResults.js │ ├── CollectionSection.js │ ├── CollectionsDropdown.js │ ├── ContentASDropdown.js │ ├── CustomCollectionResults.js │ ├── DateElement.js │ ├── EditorContextMenu.js │ ├── ExplorerPage.tsx │ ├── FieldsForm.js │ ├── HeaderSection.js │ ├── HomePage.js │ ├── IconMenu.js │ ├── LeftPaneSlim.js │ ├── LinkDropdown.js │ ├── LinkElement.js │ ├── ListItemAction.js │ ├── MainApp │ │ └── App.tsx │ ├── MentionDropdown.js │ ├── NewNodeDropdown.js │ ├── PageTabContent.js │ ├── PageTabs.js │ ├── RelatedSectionCard.js │ ├── ResultFilters.js │ ├── SpaceASResult.js │ ├── SpaceModal.tsx │ ├── SpaceOverviewSection.js │ ├── SpacePage.js │ ├── SuggestionCard.js │ ├── SuggestionsSection.js │ ├── TopBar.js │ └── Utils │ │ └── IRuminWindow.ts ├── index.tsx └── utils │ ├── date_utils.js │ └── params_utils.js ├── static └── frontend │ ├── Block.css │ ├── main.js │ ├── main.js.LICENSE.txt │ ├── main2.js │ ├── main_dev.js │ ├── react-datepicker.css │ └── style.css ├── templates └── frontend │ ├── about_validate.html │ ├── activity_detail.html │ ├── coding_lookup_article.html │ ├── coding_utility.html │ ├── coding_utility_header.html │ ├── demo_collection_block.html │ ├── demo_graph.html │ ├── demo_grid_board.html │ ├── demo_log.html │ ├── demo_log_list.html │ ├── demo_mood_board.html │ ├── demo_writer.html │ ├── explorer.html │ ├── index.html │ ├── ka_article.html │ ├── ka_articles.html │ ├── ka_articles_data.html │ ├── ka_future_of_search.html │ ├── ka_header.html │ ├── ka_more_articles.html │ ├── ka_share.html │ ├── knowledge_artist.html │ ├── new_short_link.html │ ├── space_detail.html │ ├── vaas_header.html │ └── validate_aas.html ├── tests.py ├── tsconfig.json ├── urls.py ├── views.py ├── webpack.common.js ├── webpack.config.js ├── webpack.dev.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", "@babel/preset-react" 4 | ], 5 | "plugins": ["@babel/plugin-transform-regenerator", "@babel/plugin-transform-async-to-generator"] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jhlyeung 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rumin 2 | This is the full frontend code for the [Rumin](https;//getrumin.com) web app, a visual canvas for connected ideas. 3 | 4 | It provides a workspace to visually flesh out your ideas. Rearrange them. And connect them. 5 | 6 | Rumin comes with a [web clipper browser extension](https://github.com/jhlyeung/rumin-web-clipper) (also open source), for capturing learnings as you go. 7 | 8 | ## Demo 9 | Live demo [link](https://getrumin.com/expdemo) 10 | Demo video [link](https://www.youtube.com/watch?v=ZiC2w7pPuuI) 11 | [![YouTube thumbnail for video demo](https://storage.googleapis.com/rumin-gcs-bucket/capture-sources2.png)](https://www.youtube.com/watch?v=ZiC2w7pPuuI) 12 | 13 | ### Main Components 14 | **Visual Canvas/Explorer**. The main part of the app. Use a canvas tab for each topic or work context. The canvas is built on top of [Konva](https://konvajs.org/), a useful library for manipulating HTML Canvas. Most of the Canvas-related code is in `src/components/ExplorerPage.tsx`. 15 | 16 | **Rich Text Editor**. The text editor in Rumin supports multiple media types, including images, YouTube embeds, and more. The editor is built using [Slate](https://www.slatejs.org/), which provides block-level control and is easy to extend for additional commands and functionalities. But note that Slate does have its quirks, including not working well on mobile. The editor code is in `src/components/BlockEditorSlate.js`. 17 | 18 | **Detail Page**. A page where the user can edit the body of a text/document, as well as its custom attributes. Think of this as a blend of a Word Processor and an object editor. The code is in `src/components/SpacePage.js` 19 | 20 | ## Building a Productivity App? 21 | [Say hi on Twitter](https://twitter.com/jhlyeung), and I'll see where I can help. I no longer plan on pursuing Rumin as a for-profit venture, and I would love to help where I can. 22 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-frontend/a1f5394fea71b7cab5e2217f097521816acdaee5/__init__.py -------------------------------------------------------------------------------- /admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FrontendConfig(AppConfig): 5 | name = 'frontend' 6 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --config ./webpack.dev.js --watch", 8 | "prod": "webpack --config ./webpack.prod.js --watch", 9 | "build": "webpack --config ./webpack.dev.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@amplitude/node": "^0.3.3", 16 | "@babel/core": "^7.8.4", 17 | "@babel/plugin-proposal-class-properties": "^7.8.3", 18 | "@babel/plugin-transform-async-to-generator": "^7.8.3", 19 | "@babel/plugin-transform-regenerator": "^7.8.3", 20 | "@babel/preset-env": "^7.8.4", 21 | "@babel/preset-react": "^7.8.3", 22 | "@types/body-scroll-lock": "^2.6.1", 23 | "@types/react-dom": "^16.9.8", 24 | "@types/react-modal": "^3.10.5", 25 | "@types/react-router-dom": "^5.1.5", 26 | "@types/uuid": "^8.0.0", 27 | "babel-loader": "^8.0.6", 28 | "body-scroll-lock": "^3.0.2", 29 | "core-js": "^3.6.4", 30 | "css-loader": "^3.4.2", 31 | "draft-js": "^0.11.4", 32 | "emotion": "^10.0.27", 33 | "escape-html": "^1.0.3", 34 | "is-url": "^1.2.4", 35 | "konva": "^6.0.0", 36 | "moment": "^2.24.0", 37 | "query-string": "^6.11.0", 38 | "re-resizable": "^6.3.2", 39 | "react": "^16.12.0", 40 | "react-datepicker": "^2.13.0", 41 | "react-dom": "^16.12.0", 42 | "react-konva": "^16.13.0-3", 43 | "react-modal": "^3.11.2", 44 | "react-router-dom": "^5.1.2", 45 | "regenerator-runtime": "^0.13.3", 46 | "shortid": "^2.2.15", 47 | "slate": "^0.57.1", 48 | "slate-history": "^0.57.1", 49 | "slate-hyperscript": "^0.57.1", 50 | "slate-plain-serializer": "^0.7.11", 51 | "slate-react": "^0.57.1", 52 | "terser-webpack-plugin": "^3.0.4", 53 | "tiny-tfidf": "^0.9.1", 54 | "ts-loader": "^7.0.5", 55 | "typescript": "^3.9.5", 56 | "use-image": "^1.0.6", 57 | "uuid": "^3.4.0", 58 | "webpack": "^4.41.5", 59 | "webpack-cli": "^3.3.10", 60 | "webpack-merge": "^4.2.2" 61 | }, 62 | "dependencies": { 63 | "@guestbell/slate-edit-list": "^0.3.3", 64 | "@types/react": "^16.9.36", 65 | "image-extensions": "^1.1.0", 66 | "react-digraph": "^6.6.4" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/AppContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AppContext = React.createContext({}); 4 | 5 | export const AppProvider = AppContext.Provider; 6 | export const AppConsumer = AppContext.Consumer; 7 | export default AppContext; 8 | -------------------------------------------------------------------------------- /src/components/BlockContextMenu.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect, useRef } from "react" 2 | import { SpaceASResult } from './SpaceASResult' 3 | import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock' 4 | 5 | import AppContext from './AppContext' 6 | 7 | export const BlockContextMenu = (props) => { 8 | const ref = useRef() 9 | const appContext = useContext(AppContext) 10 | const [dropdownTop, setDropdownTop] = useState(-10000) 11 | const [dropdownLeft, setDropdownLeft] = useState(0) 12 | 13 | const [currMenu, setCurrMenu] = useState('main') 14 | 15 | useEffect(() => { 16 | setDropdownLeft(props.clickCoords.x) 17 | setDropdownTop(props.clickCoords.y) 18 | }) 19 | 20 | const handleToggleClick = (e) => { 21 | e.preventDefault() 22 | e.stopPropagation() 23 | 24 | // setCurrMenu('add-to') 25 | props.handleToggleClick() 26 | } 27 | 28 | const buildMainMenu = () => { 29 | return( 30 |
39 |
43 | Collapse / expand 44 |
45 |
46 | ) 47 | } 48 | 49 | switch (currMenu) { 50 | case 'main': 51 | return buildMainMenu() 52 | // case 'move-to': 53 | // return( 54 | // 60 | // ) 61 | // case 'add-to': 62 | // return( 63 | // 70 | // ) 71 | default: 72 | return buildMainMenu() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/CapturedContentASResult.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react" 2 | 3 | export const CapturedContentASResult = (props) => { 4 | const ref = useRef() 5 | 6 | const getCapturedUrl = () => { 7 | return props.content.custom_fields && props.content.custom_fields.url ? props.content.custom_fields.url : '' 8 | } 9 | 10 | return( 11 |
props.handleClick(e, props.content)} 17 | onMouseOver={(e) => props.handleResultMouseOver(e, props.index)} 18 | onKeyUp={(e) => props.handleResultKeyUp(e, props.content, 'Activity')} 19 | > 20 |

{ props.content.title }

21 |
{ getCapturedUrl() }
22 |
23 | ) 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/components/ClickHandler.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react" 2 | 3 | import { clearAllBodyScrollLocks } from 'body-scroll-lock'; 4 | 5 | export class ClickHandler extends PureComponent { 6 | constructor(props, context) { 7 | super(props, context) 8 | 9 | this.handleClickOutside = this.handleClickOutside.bind(this) 10 | } 11 | 12 | componentDidMount() { 13 | document.addEventListener("mousedown", this.handleClickOutside) 14 | } 15 | 16 | componentWillUnmount() { 17 | document.removeEventListener("mousedown", this.handleClickOutside) 18 | } 19 | 20 | handleClickOutside(e) { 21 | if ( 22 | this.wrapperRef && 23 | !this.wrapperRef.contains(e.target) 24 | ) { 25 | 26 | clearAllBodyScrollLocks() 27 | 28 | this.props.close() 29 | } 30 | } 31 | 32 | render() { 33 | return( 34 | (this.wrapperRef = node)} 37 | style={{width: '100%', height: '100%', display: `${this.props.displayBlock ? 'block' : 'inline'}`}} 38 | >{this.props.children} 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/CollectionsDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | export const CollectionsDropdown = (props) => { 4 | const [spaceSuggestions, setSpaceSuggestions] = useState([]) // results 5 | const [query, setQuery] = useState('') 6 | const [selectedSpaces, setSelectedSpaces] = useState([]) 7 | const [hasStoragePermission, setHasStoragePermission] = useState(false) 8 | 9 | const [queryTimer, setQueryTimer] = useState(null) 10 | // const [showDropdown, setShowDropdown] = useState(false) 11 | 12 | const updateSelectedSpaces = (selected) => { 13 | setSelectedSpaces(selected) 14 | window.selectedSpaces = selected 15 | 16 | if (hasStoragePermission) { 17 | chrome.storage.local.set({ selectedSpaces: selected }) 18 | } 19 | } 20 | 21 | const handleSuggestionClick = (space) => { 22 | const updatedSelection = [...selectedSpaces, space] 23 | updateSelectedSpaces(updatedSelection) 24 | setQuery('') 25 | } 26 | 27 | const handleRemoveToken = (space) => { 28 | const updatedSelection = selectedSpaces.filter(s => s.id !== space.id) 29 | updateSelectedSpaces(updatedSelection) 30 | } 31 | 32 | const fetchAutosuggest = () => { 33 | if (query.length < 2) return 34 | 35 | fetch(`/api/v1/search/?q=${query}&lite=true`, { 36 | method: 'GET', 37 | headers: { 38 | 'Content-type': 'application/json' 39 | } 40 | }) 41 | .then(res => { 42 | if (!res.ok) throw new Error(res.status) 43 | else return res.json() 44 | }) 45 | .then(data => { 46 | setSpaceSuggestions(data.results) 47 | }) 48 | .catch(error => { 49 | console.log('error: ' + error) 50 | }) 51 | } 52 | 53 | const handleQueryKeyDown = (e) => { 54 | if (e.key === 'Backspace' && query.length === 0) { 55 | handleRemoveToken(selectedSpaces[selectedSpaces.length-1]) 56 | } 57 | 58 | if (e.key === 'Escape') { 59 | props.closeDropdown() 60 | } 61 | } 62 | 63 | const handleQueryKeyUp = () => { 64 | clearTimeout(queryTimer) 65 | const timer = setTimeout(() => { fetchAutosuggest() }, 750) 66 | setQueryTimer(timer) 67 | } 68 | 69 | const handleNewSpaceClick = () => { 70 | const body = { 71 | title: query 72 | } 73 | 74 | // fetch(`https://getrumin.com/api/v1/spaces/`, { 75 | // method: 'POST', 76 | // headers: { 77 | // 'Content-type': 'application/json' 78 | // }, 79 | // body: JSON.stringify(body) 80 | // }) 81 | // .then(res => { 82 | // if (!res.ok) throw new Error(res.status) 83 | // else return res.json() 84 | // }) 85 | // .then(space => { 86 | // // success 87 | // const updatedSelection = [...selectedSpaces, space] 88 | // updateSelectedSpaces(updatedSelection) 89 | // setQuery('') 90 | // }) 91 | // .catch(error => { 92 | // console.log('error: ' + error) 93 | // }) 94 | } 95 | 96 | const buildResults = () => { 97 | if (query.length < 2) return '' 98 | 99 | return spaceSuggestions.map(space => { 100 | return( 101 | 106 | ); 107 | }); 108 | } 109 | 110 | const buildSelectedTokens = () => { 111 | return selectedSpaces.map(space => { 112 | return( 113 |
114 | 115 | { space.title } 116 | 117 |
handleRemoveToken(space)} 120 | > 121 | 122 |
123 |
124 | ) 125 | }) 126 | } 127 | 128 | const buildASResultsSection = () => { 129 | if (query.length < 2) return '' 130 | 131 | return( 132 |
133 |
134 | { buildResults() } 135 |
136 |
137 | ) 138 | // { buildCreateCollection() } 139 | } 140 | 141 | // const buildCreateCollection = () => { 142 | // if (query.length < 1) return '' 143 | 144 | // return( 145 | //
150 | // + New page "{ query }" 151 | //
152 | // ) 153 | // } 154 | 155 | return( 156 |
157 |
160 | Filter for content connected to all of the following 161 |
162 | 163 |
164 | { buildSelectedTokens() } 165 | 166 |
167 | setQuery(e.target.value)} 172 | onKeyDown={handleQueryKeyDown} 173 | onKeyUp={handleQueryKeyUp} 174 | /> 175 |
176 |
177 | 178 | { buildASResultsSection() } 179 |
180 | ); 181 | } 182 | 183 | const SuggestionResult = (props) => { 184 | const handleClick = () => { 185 | props.handleSuggestionClick(props.space) 186 | } 187 | 188 | return( 189 |
193 | { props.space.title } 194 |
195 | ) 196 | } 197 | 198 | const SuggestedLinkToken = (props) => { 199 | return( 200 |
201 | { props.space.title } 202 |
205 | 206 |
207 |
208 | ) 209 | } 210 | -------------------------------------------------------------------------------- /src/components/ContentASDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useContext } from "react" 2 | import { CapturedContentASResult } from './CapturedContentASResult' 3 | import { SpaceASResult } from './SpaceASResult' 4 | import AppContext from './AppContext' 5 | 6 | // largely copied from LinkDropdown 7 | export const ContentASDropdown = (props) => { 8 | const ref = useRef() 9 | const inputRef = useRef() 10 | const appContext = useContext(AppContext) 11 | 12 | const [results, setResults] = useState([]) 13 | const [query, setQuery] = useState('') 14 | const [typingTimer, setTypingTimer] = useState(null) 15 | const [isFetching, setIsFetching] = useState(false) 16 | const [noResults, setNoResults] = useState(false) 17 | const [pageNum, setPageNum] = useState(1) 18 | const [activeIndex, setActiveIndex] = useState(-1) 19 | 20 | const handleKeyDown = (e) => { 21 | if (e.key === 'Escape') { 22 | props.closeDropdown() 23 | } 24 | } 25 | 26 | const fetchAutosuggest = () => { 27 | if (query.length < 2) return 28 | 29 | setIsFetching(true) 30 | 31 | fetch(`/api/v1/search?q=${query}&is_as=true&page=${pageNum}`, { 32 | method: 'GET', 33 | headers: { 34 | 'Content-type': 'application/json' 35 | } 36 | }) 37 | .then(res => { 38 | if (!res.ok) { 39 | if (res.status === 401) { 40 | this.setState({ accessDenied: true }) 41 | } 42 | throw new Error(res.status) 43 | } 44 | else return res.json() 45 | }) 46 | .then(data => { 47 | if (data.results.length === 0) { 48 | setNoResults(true) 49 | setIsFetching(false) 50 | return 51 | } 52 | 53 | if (pageNum === 1) { 54 | setResults(data.results) 55 | } 56 | else { 57 | setResults([...results, ...data.results]) 58 | } 59 | 60 | setIsFetching(false) 61 | setPageNum(pageNum+1) 62 | }) 63 | .catch(error => { 64 | console.log('error: ' + error) 65 | }) 66 | } 67 | 68 | const handleInputKeyUp = (e) => { 69 | clearTimeout(typingTimer) 70 | const timer = setTimeout(() => { fetchAutosuggest() }, 750) 71 | setTypingTimer(timer) 72 | 73 | // go up and down with arrow keys 74 | if (e.key === 'ArrowDown') { 75 | setActiveIndex(activeIndex+1) 76 | } 77 | 78 | if (e.key === 'ArrowUp') { 79 | setActiveIndex(activeIndex-1) 80 | } 81 | } 82 | 83 | const handleQueryChange = (e) => { 84 | setQuery(e.target.value) 85 | 86 | setPageNum(1) 87 | setResults([]) 88 | setActiveIndex(-1) 89 | } 90 | 91 | const handleActivityResultClick = (e, activity) => { 92 | props.selectContent(activity) 93 | props.closeDropdown() 94 | } 95 | 96 | const handleSpaceResultClick = (e, space) => { 97 | props.selectContent(space) 98 | props.closeDropdown() 99 | } 100 | 101 | const handleResultMouseOver = (e, index) => { 102 | setActiveIndex(index) 103 | } 104 | 105 | const handleScroll = (e) => { 106 | const remainingScrollHeight = e.target.scrollHeight - e.target.scrollTop 107 | const bottom = (remainingScrollHeight - e.target.clientHeight) < 50 108 | 109 | if (!bottom || isFetching || results.length === 0) return 110 | 111 | fetchAutosuggest() 112 | } 113 | 114 | const handleResultKeyUp = (e, obj, objType) => { 115 | if (e.key === 'ArrowDown') { 116 | e.preventDefault() 117 | setActiveIndex(activeIndex+1) 118 | } 119 | 120 | if (e.key === 'ArrowUp') { 121 | e.preventDefault() 122 | 123 | if (activeIndex === 0) { 124 | inputRef.current.focus() 125 | } 126 | 127 | setActiveIndex(activeIndex-1) 128 | } 129 | 130 | if (e.key === 'Enter') { 131 | e.preventDefault() 132 | 133 | if (objType === 'Activity') { 134 | handleActivityResultClick(e, obj) 135 | } 136 | else { 137 | handleSpaceResultClick(e, obj) 138 | } 139 | } 140 | } 141 | 142 | const buildResults = () => { 143 | if (query.length < 2) { 144 | 145 | return '' 146 | } 147 | 148 | const asResults = results.map((result, index) => { 149 | const buildContent = { 150 | Activity: () => { 151 | return( 152 | 161 | ) 162 | }, 163 | Space: () => { 164 | return( 165 | 174 | ) 175 | } 176 | }[result.content_type] 177 | 178 | return buildContent() 179 | }) 180 | 181 | return( 182 |
185 |
186 | Matching thoughts 187 |
188 |
189 | { asResults } 190 |
191 |
192 | ) 193 | } 194 | 195 | const buildLoadingGIF = () => { 196 | if (query.length < 2) return '' 197 | 198 | let message = '' 199 | 200 | return( 201 |
202 | { message } 203 | 209 |
210 | ) 211 | } 212 | 213 | const buildLoadingMoreIndicator = () => { 214 | if (isFetching) { 215 | return buildLoadingGIF() 216 | } 217 | 218 | if (noResults === true) { 219 | return('') 220 | } 221 | } 222 | 223 | const buildNewSearch = () => { 224 | if (query.length < 2) return '' 225 | 226 | const newTabUrl = `/?q=${query}` 227 | 228 | return( 229 | 230 |
231 | Search in new tab "{ query }" 232 |
233 |
234 | ) 235 | } 236 | 237 | return( 238 |
241 |
242 | setActiveIndex(-1)} 251 | /> 252 |
253 | { buildResults() } 254 | { buildLoadingMoreIndicator() } 255 |
256 | ) 257 | // { buildNewSearch() } 258 | } 259 | -------------------------------------------------------------------------------- /src/components/CustomCollectionResults.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { friendlyDateTimeStr } from '../utils/date_utils' 3 | import { ClickHandler } from './ClickHandler' 4 | 5 | import { Resizable, ResizeCallback } from 're-resizable' 6 | import shortid from 'shortid' 7 | 8 | export const CustomCollectionResults = (props) => { 9 | const getCollection = () => { 10 | return props.content.custom_fields[props.fieldName] 11 | } 12 | 13 | const buildComments = () => { 14 | return getCollection().map(result => { 15 | let url = '' 16 | let body = '' 17 | 18 | if (result.url) { 19 | url = ( 20 |
21 | Open link 22 |
23 | ) 24 | } 25 | 26 | return( 27 |
28 | { url } 29 | { result.body || '' } 30 |
31 | ) 32 | }) 33 | } 34 | 35 | const buildMessages = () => { 36 | return getCollection().map(result => { 37 | let url = '' 38 | let body = '' 39 | 40 | return( 41 |
42 |
43 | From { result.sender_name } · 44 | Open link 45 | 46 |
47 |
48 | { result.message_text } 49 |
50 |
51 | ) 52 | }) 53 | } 54 | 55 | const buildResults = () => { 56 | switch (props.fieldName) { 57 | case 'comments': 58 | return buildComments() 59 | case 'messages': 60 | return buildMessages() 61 | default: 62 | return '' 63 | } 64 | } 65 | 66 | return( 67 |
68 | { buildResults() } 69 |
70 | ) 71 | } 72 | 73 | // const CommentCard = () => { 74 | 75 | // } 76 | -------------------------------------------------------------------------------- /src/components/DateElement.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react" 2 | import { ClickHandler } from './ClickHandler' 3 | import DatePicker from "react-datepicker" 4 | import { friendlyDateStr } from '../utils/date_utils' 5 | import { 6 | useSlate 7 | } from "slate-react" 8 | 9 | import moment from 'moment' 10 | 11 | export const DateElement = (props) => { 12 | const editor = useSlate() 13 | 14 | const [showDropdown, setShowDropdown] = useState(false) 15 | 16 | const handleMouseDown = (e) => { 17 | e.preventDefault() 18 | props.setPersistedSelection(editor.selection) 19 | setShowDropdown(true) 20 | } 21 | 22 | const getParsedDate = () => { 23 | return moment(props.element.dateISOString) 24 | } 25 | 26 | const getDisplayStr = () => { 27 | return friendlyDateStr(getParsedDate()) 28 | } 29 | 30 | const overriddenChildren = () => { 31 | const newNode = { 32 | ...props.children.props.node, 33 | children: [{ text: getDisplayStr() }] 34 | } 35 | 36 | const newChildren = { 37 | ...props.children, 38 | props: { 39 | ...props.children.props, 40 | node: newNode 41 | } 42 | } 43 | console.log(props.children, newChildren) 44 | 45 | return newChildren //props.children 46 | } 47 | 48 | const handleMouseOver = () => { 49 | if (getDisplayStr() !== props.element.children[0].text) { 50 | props.updateDateStr(props.element, getDisplayStr()) 51 | } 52 | } 53 | 54 | const handleDateChange = (date) => { 55 | props.updateDate(props.element, date) 56 | } 57 | 58 | const buildDropdown = () => { 59 | if (!showDropdown) return '' 60 | 61 | return ( 62 | setShowDropdown(false)} 64 | parsedDate={getParsedDate()} 65 | handleDateChange={handleDateChange} 66 | /> 67 | ) 68 | } 69 | 70 | // console.log('props.children in DateElement', props.children) 71 | 72 | return( 73 | 74 | 81 | { props.children } 82 | 83 | { buildDropdown() } 84 | 85 | ) 86 | // { getDisplayStr() } 87 | } 88 | 89 | const DateDropdown = (props) => { 90 | const [pickedDate, setPickedDate] = useState(props.parsedDate.toDate()) 91 | 92 | const handleDateChange = (date) => { 93 | setPickedDate(date) 94 | } 95 | 96 | const closeMenu = () => { 97 | // TODO - update and save 98 | props.handleDateChange(pickedDate) 99 | props.closeDropdown() 100 | } 101 | 102 | return( 103 | 106 |
111 | 115 |
116 |
117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /src/components/EditorContextMenu.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useContext } from "react" 2 | import { MoveToMenu } from './CardContextMenu' 3 | import AppContext from './AppContext' 4 | 5 | export const EditorContextMenu = (props) => { 6 | const ref = useRef() 7 | const appContext = useContext(AppContext) 8 | const [dropdownTop, setDropdownTop] = useState(-10000) 9 | const [dropdownLeft, setDropdownLeft] = useState(0) 10 | 11 | const [isDropdownTopSet, setIsDropdownTopSet] = useState(false) 12 | 13 | const [currMenu, setCurrMenu] = useState('main') 14 | 15 | useEffect(() => { 16 | setDropdownLeft(props.clickCoords.x) 17 | setDropdownTop(props.clickCoords.y) 18 | 19 | setIsDropdownTopSet(true) 20 | }) 21 | 22 | const handleMoveToClick = (e) => { 23 | e.preventDefault() 24 | e.stopPropagation() 25 | 26 | setCurrMenu('move-to') 27 | } 28 | 29 | const handleTurnIntoPageClick = () => { 30 | props.turnIntoPage() 31 | props.closeDropdown() 32 | } 33 | 34 | const handleMoveToResultClick = (e, space) => { 35 | props.moveBlocksToSpace(space) 36 | } 37 | 38 | const buildMainMenu = () => { 39 | return( 40 |
49 |
53 | Turn into page 54 |
55 |
59 | 60 | Move to 61 |
62 |
63 | ) 64 | } 65 | 66 | switch (currMenu) { 67 | case 'main': 68 | return buildMainMenu() 69 | case 'move-to': 70 | return( 71 | 79 | ) 80 | default: 81 | return buildMainMenu() 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/components/FieldsForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useContext, useEffect } from 'react' 2 | 3 | import { ClickHandler } from './ClickHandler' 4 | 5 | export const FieldsForm = props => { 6 | const [showForm, setShowForm] = useState(false) 7 | 8 | const [fields, setFields] = useState(props.filteredFields || props.content.custom_fields) 9 | const [fieldTypes, setFieldTypes] = useState(props.content.custom_field_types) 10 | const [fieldNames, setFieldNames] = useState(Object.keys(fields)) // maintain the order of fields on the UI 11 | 12 | const [saveState, setSaveState] = useState('unsaved') 13 | const [saveTimer, setSaveTimer] = useState(null) 14 | 15 | const handleAddPropertyClick = () => { 16 | setFields({ 17 | ...fields, 18 | '': '' 19 | }) 20 | 21 | if (fieldNames.indexOf('') < 0) { 22 | setFieldNames([...fieldNames, '']) 23 | } 24 | } 25 | 26 | const updateFieldName = (fieldName, newName) => { 27 | // update fields and field_types 28 | let updatedFields = Object.assign({}, fields) 29 | let updatedTypes = Object.assign({}, fieldTypes) 30 | 31 | // update fieldNames 32 | const fieldIndex = fieldNames.indexOf(fieldName) 33 | setFieldNames([...fieldNames.slice(0, fieldIndex), newName, ...fieldNames.slice(fieldIndex+1,)]) 34 | 35 | const value = updatedFields[fieldName] 36 | const type = updatedTypes[fieldName] || 'Text' 37 | 38 | delete updatedFields[fieldName] 39 | delete updatedTypes[fieldName] 40 | 41 | updatedFields[newName] = value 42 | updatedTypes[newName] = type 43 | 44 | setFields(updatedFields) 45 | setFieldTypes(updatedTypes) 46 | } 47 | 48 | const updateFieldValue = (fieldName, newValue) => { 49 | let updatedFields = Object.assign({}, fields) 50 | updatedFields[fieldName] = newValue 51 | setFields(updatedFields) 52 | } 53 | 54 | const updateFieldType = (fieldName, fieldType) => { 55 | let updatedTypes = Object.assign({}, fieldTypes) 56 | updatedTypes[fieldName] = fieldType 57 | setFieldTypes(updatedTypes) 58 | // props.updateCustomFieldTypes(updated) 59 | } 60 | 61 | const handleSaveFieldsClick = () => { 62 | const body = { 63 | custom_fields: fields, 64 | custom_field_types: fieldTypes 65 | } 66 | 67 | setSaveState('saving') 68 | 69 | const url = props.content.content_type === 'Space' ? `/api/v1/spaces/${props.content.id}/` : `/api/v1/activities/${props.content.id}/` 70 | fetch(url, { 71 | method: 'PATCH', 72 | headers: { 73 | 'Content-type': 'application/json' 74 | }, 75 | body: JSON.stringify(body) 76 | }) 77 | .then(res => { 78 | if (!res.ok) throw new Error(res.status) 79 | else return res.json() 80 | }) 81 | .then(data => { 82 | setSaveState('saved') 83 | 84 | // reset the save button in 2 seconds 85 | clearTimeout(saveTimer) 86 | const timer = setTimeout(() => { setSaveState('unsaved') }, 2000) 87 | setSaveTimer(timer) 88 | 89 | props.updateCustomFields(fields, fieldTypes) 90 | }) 91 | .catch(error => { 92 | console.log('error: ' + error) 93 | }) 94 | } 95 | 96 | const handleFieldNameChange = (fieldName, newName) => { 97 | updateFieldName(fieldName, newName) 98 | } 99 | 100 | const handleFieldValueChange = (fieldName, newValue) => { 101 | updateFieldValue(fieldName, newValue) 102 | } 103 | 104 | const handleFieldTypeChange = (fieldName, fieldType) => { 105 | updateFieldType(fieldName, fieldType) 106 | } 107 | 108 | const buildFields = () => { 109 | return fieldNames.map(fieldName => { 110 | const fieldValue = fields[fieldName] 111 | 112 | return( 113 |
114 | 123 |
124 | ) 125 | }) 126 | } 127 | 128 | const buildSaveBtn = () => { 129 | if (saveState === 'saving') { 130 | return( 131 |
132 | Saving... 133 |
134 | ) 135 | } 136 | 137 | if (saveState === 'saved') { 138 | return( 139 |
140 | Saved 141 |
142 | ) 143 | } 144 | 145 | return( 146 |
151 | Save properties 152 |
153 | ) 154 | } 155 | 156 | return( 157 |
158 | { buildFields() } 159 | 160 |
161 |
165 | + Add a property 166 |
167 | 168 | { buildSaveBtn() } 169 |
170 |
171 | ) 172 | } 173 | 174 | const CustomField = (props) => { 175 | const buildLink = () => { 176 | if (typeof(props.fieldValue) !== 'string' || !props.fieldValue.startsWith('https://')) return '' 177 | 178 | return( 179 | 184 | ) 185 | } 186 | 187 | return( 188 |
189 | 195 |
196 | props.handleFieldNameChange(props.fieldName, e.target.value)} 202 | onBlur={() => props.handleFieldNameChange(props.fieldName, props.fieldName.trim())} 203 | /> 204 |
205 |
206 | props.handleFieldValueChange(props.fieldName, e.target.value)} 214 | /> 215 | { buildLink() } 216 |
217 |
218 | ) 219 | } 220 | 221 | const FieldTypeOption = (props) => { 222 | const handleOptionClick = () => { 223 | props.handleFieldTypeChange(props.fieldName, props.fieldType) 224 | props.closeDropdown() 225 | } 226 | 227 | return( 228 |
235 |
{ props.fieldType }
236 |
237 | ) 238 | } 239 | 240 | const FieldTypeDropdown = (props) => { 241 | const inputRef = useRef() 242 | const [query, setQuery] = useState('') 243 | const [typingTimer, setTypingTimer] = useState(null) 244 | 245 | const fetchAutosuggest = () => { 246 | 247 | } 248 | 249 | const handleInputKeyUp = (e) => { 250 | clearTimeout(typingTimer) 251 | const timer = setTimeout(() => { fetchAutosuggest() }, 750) 252 | setTypingTimer(timer) 253 | } 254 | 255 | const buildFieldOptions = () => { 256 | return( 257 |
260 |
261 | 267 | 273 |
274 |
275 | ) 276 | } 277 | 278 | return( 279 |
280 | { buildFieldOptions() } 281 |
282 | ) 283 | } 284 | 285 | const FieldTypeBtn = (props) => { 286 | const [showDropdown, setShowDropdown] = useState(false) 287 | 288 | const closeDropdown = () => { 289 | setShowDropdown(false) 290 | } 291 | 292 | const buildFieldTypeIcon = () => { 293 | const icon = { 294 | Text: , 295 | Number: , 296 | }[props.fieldType] 297 | 298 | return icon 299 | } 300 | 301 | const buildPropTypeDropdown = () => { 302 | if (!showDropdown) return '' 303 | 304 | return( 305 | {setShowDropdown(false)}} 307 | > 308 | 312 | 313 | ) 314 | } 315 | 316 | return( 317 |
318 |
setShowDropdown(true)} 322 | > 323 | { buildFieldTypeIcon() } 324 |
325 | { buildPropTypeDropdown() } 326 |
327 | ) 328 | } 329 | -------------------------------------------------------------------------------- /src/components/IconMenu.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { ClickHandler } from './ClickHandler' 3 | 4 | export const IconMenu = (props) => { 5 | const [showDropdown, setShowDropdown] = useState(false) 6 | const [isSaving, setIsSaving] = useState(false) 7 | 8 | const handleIconSubmit = (iconData) => { 9 | const body = { icon: iconData } 10 | 11 | setIsSaving(true) 12 | 13 | fetch(`/api/v1/spaces/${props.space.id}/`, { 14 | method: 'PATCH', 15 | headers: { 16 | 'Content-type': 'application/json' 17 | }, 18 | body: JSON.stringify(body) 19 | }) 20 | .then(res => { 21 | if (!res.ok) { 22 | if (res.status === 401) {} 23 | throw new Error(res.status) 24 | } 25 | else return res.json() 26 | }) 27 | .then(data => { 28 | setShowDropdown(false) 29 | setIsSaving(false) 30 | 31 | props.updateSpace(data) 32 | }) 33 | .catch(error => { 34 | 35 | }) 36 | } 37 | 38 | const buildDropdown = () => { 39 | if (!showDropdown) return '' 40 | 41 | return( 42 | setShowDropdown(false)} 44 | > 45 | 49 | 50 | ) 51 | } 52 | 53 | if (props.space.icon) { 54 | return( 55 |
setShowDropdown(true)} 58 | > 59 | 63 | 64 | { buildDropdown() } 65 |
66 | ) 67 | } 68 | 69 | return( 70 |
71 |
setShowDropdown(true)} 74 | > 75 | Add icon 76 |
77 | 78 | { buildDropdown() } 79 |
80 | ) 81 | } 82 | 83 | export const IconDropdown = (props) => { 84 | const [iconUrl, setIconUrl] = useState('') 85 | 86 | const handleIconPaste = (e) => { 87 | // pasting from clipboard 88 | console.log('pasting image for icon', e, e.clipboardData, e.clipboardData.items) 89 | 90 | const dataTransfer = e.clipboardData.items[0] 91 | if (dataTransfer && dataTransfer.type === 'image/png') { 92 | const blob = dataTransfer.getAsFile() 93 | const fileReader = new FileReader() 94 | 95 | // send as raw base64 data 96 | fileReader.onloadend = () => { 97 | props.handleIconSubmit(fileReader.result) 98 | } 99 | fileReader.readAsDataURL(blob) 100 | } 101 | } 102 | 103 | const handleIconChange = (e) => { 104 | setIconUrl(e.target.value) 105 | } 106 | 107 | if (props.isSaving) { 108 | return( 109 |
110 | Saving... 111 |
112 | ) 113 | } 114 | 115 | return( 116 |
117 |
118 | 125 |
126 | 127 |
128 |
props.handleIconSubmit(iconUrl)} 132 | >Submit
133 |
134 | 135 |
136 |

Paste an image link, or a copied image.

137 | 138 |

Works with any image from the web and screenshots

139 |
140 |
141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /src/components/LeftPaneSlim.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react" 2 | import { ClickHandler } from './ClickHandler' 3 | import uuidv4 from "uuid/v4"; 4 | import queryString from 'query-string' 5 | 6 | import AppContext from './AppContext' 7 | 8 | export const LeftPaneSlim = (props) => { 9 | const [favoriteSpaces, setFavoriteSpaces] = useState([]) 10 | const [isFetchingFavoriteSpaces, setIsFetchingFavoriteSpaces] = useState(false) 11 | const [hasFetchedFavoriteSpaces, setHasFetchedFavoriteSpaces] = useState(false) 12 | 13 | const appContext = useContext(AppContext) 14 | 15 | const isMobile = () => { 16 | return window.django.is_mobile 17 | } 18 | 19 | const [isCollapsed, setIsCollapsed] = useState(isMobile()) 20 | 21 | const handleClose = () => { 22 | if (!isMobile()) return 23 | 24 | setIsCollapsed(true) 25 | } 26 | 27 | const isHomePage = () => { 28 | return location.pathname === '/' && location.search === '' 29 | } 30 | 31 | const isCapturedContentPage = () => { 32 | return queryString.parse(location.search).content_type === 'Activity' 33 | } 34 | 35 | const userIsAuthenticated = () => { 36 | return window.django.user.is_authenticated 37 | } 38 | 39 | const getUserFirstName = () => { 40 | return window.django.user.first_name 41 | } 42 | 43 | const getUserInitial = () => { 44 | return getUserFirstName()[0] 45 | } 46 | 47 | const getStyle = () => { 48 | if (!isMobile()) return {} 49 | 50 | if (isCollapsed) { 51 | return({ 52 | width: '0', 53 | display: 'none' 54 | }) 55 | } 56 | else { 57 | return({ 58 | width: '65%', 59 | boxShadow: '5px 0px 10px #ccc' 60 | }) 61 | } 62 | } 63 | 64 | return( 65 |
66 |
setIsCollapsed(!isCollapsed)} 69 | style={isMobile() ? {} : {display: 'none'}} 70 | > 71 | 72 |
73 | 76 |
77 | 78 |
79 | 80 | 81 | 82 |
83 |
84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/LinkElement.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from "react" 2 | import { ClickHandler } from './ClickHandler' 3 | import { BlockEditorSlate } from './BlockEditorSlate' 4 | 5 | import moment from 'moment' 6 | 7 | import AppContext from "./AppContext" 8 | 9 | export const LinkElement = props => { 10 | const appContext = useContext(AppContext) 11 | 12 | const [hoverTimer, setHoverTimer] = useState(null) 13 | const [showLinkPreview, setShowLinkPreview] = useState(false) 14 | const [isClicked, setIsClicked] = useState(false) 15 | 16 | const handleClick = () => { 17 | // location.href = props.url 18 | setIsClicked(true) 19 | setShowLinkPreview(true) 20 | } 21 | 22 | const handleOutsideClick = () => { 23 | setIsClicked(false) 24 | setShowLinkPreview(false) 25 | } 26 | 27 | const handleKeyDown = (e) => { 28 | if (e.key === 'Escape') { 29 | setShowLinkPreview(false) 30 | } 31 | } 32 | 33 | const handleMouseEnter = () => { 34 | if (!showLinkPreview) { 35 | clearTimeout(hoverTimer) 36 | const timer = setTimeout(() => { setShowLinkPreview(true) }, 750) 37 | setHoverTimer(timer) 38 | } 39 | } 40 | 41 | const handleMouseLeave = () => { 42 | if (!isClicked) { 43 | clearTimeout(hoverTimer) 44 | setShowLinkPreview(false) 45 | } 46 | } 47 | 48 | const buildLinkPreview = () => { 49 | if (!showLinkPreview) return '' 50 | 51 | return( 52 | { handleOutsideClick() }} 54 | > 55 | 61 | 62 | ) 63 | } 64 | 65 | if (props.readOnly) { 66 | return( 67 | {props.children} 71 | ) 72 | } 73 | 74 | 75 | return( 76 | 79 | 87 | {props.children} 88 | 89 | { buildLinkPreview() } 90 | 91 | ); 92 | } 93 | 94 | const LinkPreview = (props) => { 95 | const appContext = useContext(AppContext) 96 | const [isFetching, setIsFetching] = useState(false) 97 | 98 | const spaceRegex = () => { 99 | return /(\/spaces\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/ 100 | } 101 | 102 | const activityRegex = () => { 103 | return /(\/activities\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/ 104 | } 105 | 106 | const isActivity = () => { 107 | const re = activityRegex() 108 | return !!re.exec(props.url) 109 | } 110 | 111 | const getActivityId = () => { 112 | const re = activityRegex() 113 | const m = re.exec(props.url) 114 | if (!m) return null 115 | 116 | const parts = m[0].split('/') 117 | return parts[parts.length-1] 118 | } 119 | 120 | const isSpace = () => { 121 | const re = spaceRegex() 122 | return !!re.exec(props.url) 123 | } 124 | 125 | const getSpaceId = () => { 126 | const re = spaceRegex() 127 | const m = re.exec(props.url) 128 | if (!m) return null 129 | 130 | const parts = m[0].split('/') 131 | return parts[parts.length-1] 132 | } 133 | 134 | const buildUpdatedAtTimestamp = (space) => { 135 | const updatedAt = moment(space.updated_at); 136 | return updatedAt.calendar(); 137 | } 138 | 139 | const buildSpacePreview = (space) => { 140 | if (space) { 141 | return( 142 |
143 | 149 |
150 | last updated { buildUpdatedAtTimestamp(space) } 151 |
152 |
153 | 162 |
163 |
164 | ) 165 | } 166 | 167 | return( 168 |
169 | 176 |
177 | ) 178 | // 179 | 180 | // { space.text_body.length > 500 ? `${space.text_body.slice(0,500)}...` : space.text_body } 181 | } 182 | 183 | const buildActivityPreview = (activity) => { 184 | let domain = '' 185 | if (activity.url) { 186 | const domainRe = /^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/ 187 | const m = domainRe.exec(activity.url) 188 | domain = m[1] 189 | } 190 | 191 | // 192 | return( 193 |
194 | {activity.title} 198 | 199 |
{ domain }
200 |
201 | { activity.text_body.length > 250 ? `${activity.text_body.slice(0,250)}...` : activity.text_body } 202 |
203 |
204 | ) 205 | } 206 | 207 | const buildPreview = () => { 208 | if (appContext.pagePreviews[props.url]) { 209 | const obj = appContext.pagePreviews[props.url] 210 | 211 | if (isSpace()) { 212 | return buildSpacePreview(obj) 213 | } 214 | if (isActivity()) { 215 | return buildActivityPreview(obj) 216 | } 217 | } 218 | 219 | return( 220 | {props.url} 225 | ) 226 | } 227 | 228 | 229 | useEffect(() => { 230 | // if (props.readOnly) return 231 | 232 | if (props.disableLinkFetching || appContext.pagePreviews[props.url] || isFetching) return 233 | 234 | setIsFetching(true) 235 | 236 | if (isSpace() && getSpaceId()) { 237 | fetch(`/api/v1/spaces/${getSpaceId()}/`, { 238 | method: 'GET', 239 | headers: { 240 | 'Content-type': 'application/json' 241 | } 242 | }) 243 | .then(res => { 244 | if (!res.ok) throw new Error(res.status) 245 | else return res.json() 246 | }) 247 | .then(data => { 248 | appContext.updatePagePreviews(props.url, data) 249 | setIsFetching(false) 250 | }) 251 | .catch(error => { 252 | console.log('error: ' + error) 253 | }) 254 | } 255 | 256 | if (isActivity() && getActivityId()) { 257 | fetch(`/api/v1/activities/${getActivityId()}/`, { 258 | method: 'GET', 259 | headers: { 260 | 'Content-type': 'application/json' 261 | } 262 | }) 263 | .then(res => { 264 | if (!res.ok) throw new Error(res.status) 265 | else return res.json() 266 | }) 267 | .then(data => { 268 | appContext.updatePagePreviews(props.url, data) 269 | setIsFetching(false) 270 | }) 271 | .catch(error => { 272 | console.log('error: ' + error) 273 | }) 274 | } 275 | 276 | }) 277 | 278 | return( 279 |
284 | { buildPreview() } 285 |
286 | ) 287 | } 288 | 289 | -------------------------------------------------------------------------------- /src/components/ListItemAction.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import { ClickHandler } from './ClickHandler' 4 | 5 | export const CopyLinkAction = props => { 6 | const urlRef = React.createRef() 7 | const [showDropdown, setShowDropdown] = useState(false) 8 | const [copySuccess, setCopySuccess] = useState('') 9 | 10 | const handleClick = () => { 11 | setShowDropdown(true) 12 | 13 | urlRef.current.select() 14 | document.execCommand('copy') 15 | setCopySuccess('link copied to clipboard') 16 | } 17 | 18 | const buildDropdown = () => { 19 | return( 20 | setShowDropdown(false) } 22 | > 23 |
24 | 30 | { copySuccess } 31 |
32 |
33 | ) 34 | } 35 | 36 | return( 37 |
38 |
43 | 44 |
45 | 46 | { buildDropdown() } 47 |
48 | ); 49 | } 50 | 51 | export const CopyContentAction = props => { 52 | const [showDropdown, setShowDropdown] = useState(false) 53 | 54 | const buildDropdown = () => { 55 | return( 56 | setShowDropdown(false) } 58 | > 59 |
60 | "Copy content" feature is coming soon! 61 |
62 |
63 | ) 64 | } 65 | 66 | return( 67 |
68 |
setShowDropdown(!showDropdown)} 71 | title="Copy content" 72 | > 73 | 74 |
75 | 76 | { buildDropdown() } 77 |
78 | ); 79 | } 80 | 81 | export const ArchiveAction = props => { 82 | return( 83 |
84 |
props.handleArchive()} 87 | title="Archive" 88 | > 89 | 90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/MentionDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useContext } from "react" 2 | import { SpaceASResult } from './SpaceASResult' 3 | import AppContext from './AppContext' 4 | import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock' 5 | 6 | export const MentionDropdown = (props) => { 7 | const ref = useRef() 8 | const inputRef = useRef() 9 | const appContext = useContext(AppContext) 10 | 11 | const [results, setResults] = useState([]) 12 | const [query, setQuery] = useState('') 13 | const [typingTimer, setTypingTimer] = useState(null) 14 | const [isFetching, setIsFetching] = useState(false) 15 | const [noResults, setNoResults] = useState(false) 16 | const [pageNum, setPageNum] = useState(1) 17 | const [dropdownTop, setDropdownTop] = useState(-10000) 18 | const [dropdownLeft, setDropdownLeft] = useState(0) 19 | const [isDropdownTopSet, setIsDropdownTopSet] = useState(false) 20 | 21 | const [activeIndex, setActiveIndex] = useState(-1) 22 | 23 | const [furthestCursorOffset, setFurthestCursorOffset] = useState(0) 24 | const [editorCurrPoint, setEditorCurrPoint] = useState(null) 25 | 26 | useEffect(() => { 27 | const el = ref.current 28 | if (!el || !props.domRange || isDropdownTopSet === true) return 29 | 30 | const rect = props.domRange.getBoundingClientRect() 31 | 32 | let left = rect.left + window.pageXOffset - el.offsetWidth / 2 + rect.width / 2 33 | if (left < 15) { 34 | left = 15 35 | } 36 | 37 | let top = rect.top + window.pageYOffset - el.offsetHeight + 80 38 | if (top + 200 > window.innerHeight) { 39 | top -= (top + 200 - window.innerHeight) 40 | } 41 | setDropdownTop(top) // - 150) 42 | setDropdownLeft(left) 43 | 44 | setIsDropdownTopSet(true) 45 | // setDropdownLeft(rect.left + window.pageXOffset - el.offsetWidth/4) 46 | // el.style.top = `${rect.top + window.pageYOffset - el.offsetHeight - 100}px` 47 | // el.style.left = `${rect.left + window.pageXOffset - el.offsetWidth}` 48 | 49 | // disableBodyScroll(ref.current) 50 | }, []) 51 | 52 | useEffect(() => { 53 | if (props.currPoint) { 54 | setEditorCurrPoint(props.currPoint) 55 | 56 | // selected text 57 | props.getEditorSelectedText(props.startPoint, props.currPoint) 58 | } 59 | }, [props.currPoint]) 60 | 61 | // const handleEnter = () => { 62 | // if (query.startsWith('http://') || query.startsWith('https://') || query.startsWith('/') || query.startsWith('mailto:')) { 63 | // props.linkUrl(query) 64 | // } 65 | // else { 66 | // props.linkTitle(query) 67 | // } 68 | // props.closeDropdown() 69 | // } 70 | 71 | const handleKeyDown = (e) => { 72 | if (e.key === 'Escape') { 73 | props.closeDropdown() 74 | } 75 | 76 | if (e.key === 'Enter') { 77 | e.preventDefault() 78 | 79 | handleEnter() 80 | } 81 | } 82 | 83 | // const fetchAutosuggest = () => { 84 | // if (query.length < 2) return 85 | 86 | // setIsFetching(true) 87 | 88 | // fetch(`/api/v1/search?q=${query}&is_as=true&page=${pageNum}`, { 89 | // method: 'GET', 90 | // headers: { 91 | // 'Content-type': 'application/json' 92 | // } 93 | // }) 94 | // .then(res => { 95 | // if (!res.ok) { 96 | // if (res.status === 401) { 97 | // this.setState({ accessDenied: true }) 98 | // } 99 | // throw new Error(res.status) 100 | // } 101 | // else return res.json() 102 | // }) 103 | // .then(data => { 104 | // if (data.results.length === 0) { 105 | // setNoResults(true) 106 | // setIsFetching(false) 107 | // return 108 | // } 109 | 110 | // if (pageNum === 1) { 111 | // setResults(data.results) 112 | // } 113 | // else { 114 | // setResults([...results, ...data.results]) 115 | // } 116 | 117 | // setIsFetching(false) 118 | // setPageNum(pageNum+1) 119 | // }) 120 | // .catch(error => { 121 | // console.log('error: ' + error) 122 | // }) 123 | // } 124 | 125 | const handleInputKeyUp = (e) => { 126 | // clearTimeout(typingTimer) 127 | // const timer = setTimeout(() => { fetchAutosuggest() }, 750) 128 | // setTypingTimer(timer) 129 | 130 | // go up and down with arrow keys 131 | if (e.key === 'ArrowDown') { 132 | setActiveIndex(activeIndex+1) 133 | } 134 | 135 | if (e.key === 'ArrowUp') { 136 | setActiveIndex(activeIndex-1) 137 | } 138 | } 139 | 140 | const handleQueryChange = (e) => { 141 | setQuery(e.target.value) 142 | 143 | setPageNum(1) 144 | setResults([]) 145 | setActiveIndex(-1) 146 | } 147 | 148 | const linkUrl = (url, label) => { 149 | props.linkUrl(url, label) 150 | props.closeDropdown() 151 | } 152 | 153 | const handleActivityResultClick = (e, activity) => { 154 | props.linkUrl(`/activities/${activity.id}`, activity.title) 155 | props.closeDropdown() 156 | } 157 | 158 | const linkToSpace = (space) => { 159 | props.linkUrl(`/spaces/${space.id}`, space.title) 160 | } 161 | 162 | const handleSpaceResultClick = (e, space) => { 163 | linkToSpace(space) 164 | props.closeDropdown() 165 | } 166 | 167 | const handleResultMouseOver = (e, index) => { 168 | setActiveIndex(index) 169 | } 170 | 171 | // FIXME - ??? 172 | const handleLinkClick = () => { 173 | props.linkUrl(query) 174 | props.closeDropdown() 175 | } 176 | 177 | const handleUnlinkClick = () => { 178 | props.unlinkUrl() 179 | props.closeDropdown() 180 | } 181 | 182 | const handleCreateNewSpaceClick = () => { 183 | const body = { 184 | title: query, 185 | parent_id: props.space ? props.space.id : null 186 | } 187 | 188 | fetch(`/api/v1/spaces/`, { 189 | method: 'POST', 190 | headers: { 191 | 'Content-type': 'application/json' 192 | }, 193 | body: JSON.stringify(body) 194 | }) 195 | .then(res => { 196 | if (!res.ok) throw new Error(res.status) 197 | else return res.json() 198 | }) 199 | .then(space => { 200 | // success 201 | linkToSpace(space) 202 | props.closeDropdown() 203 | }) 204 | .catch(error => { 205 | console.log('error: ' + error) 206 | }) 207 | } 208 | 209 | const handleScroll = (e) => { 210 | const remainingScrollHeight = e.target.scrollHeight - e.target.scrollTop 211 | const bottom = (remainingScrollHeight - e.target.clientHeight) < 50 212 | 213 | if (!bottom || isFetching || results.length === 0) return 214 | 215 | // fetchAutosuggest() 216 | } 217 | 218 | const handleResultKeyUp = (e, obj, objType) => { 219 | // if (e.key === 'ArrowDown') { 220 | // e.preventDefault() 221 | // setActiveIndex(activeIndex+1) 222 | // } 223 | 224 | // if (e.key === 'ArrowUp') { 225 | // e.preventDefault() 226 | 227 | // if (activeIndex === 0) { 228 | // inputRef.current.focus() 229 | // } 230 | 231 | // setActiveIndex(activeIndex-1) 232 | // } 233 | 234 | // if (e.key === 'Enter') { 235 | // e.preventDefault() 236 | 237 | // if (objType === 'Activity') { 238 | // handleActivityResultClick(e, obj) 239 | // } 240 | // else { 241 | // handleSpaceResultClick(e, obj) 242 | // } 243 | // } 244 | } 245 | 246 | const handleTodayClick = () => { 247 | const date = new Date() 248 | props.selectDate(props.startPoint, editorCurrPoint, date) 249 | props.closeDropdown() 250 | } 251 | 252 | const getQueryText = () => { 253 | return props.getEditorSelectedText(props.startPoint, props.currPoint) 254 | } 255 | 256 | return( 257 |
266 |
270 | Type to mention a date 271 |
272 | 273 |
278 | Today 279 |
280 |
284 | Pick a date 285 |
286 |
287 | ) 288 | } 289 | -------------------------------------------------------------------------------- /src/components/NewNodeDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useContext } from "react" 2 | import { CapturedContentASResult } from './CapturedContentASResult' 3 | import { SpaceASResult } from './SpaceASResult' 4 | import AppContext from './AppContext' 5 | import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock' 6 | 7 | export const NewNodeDropdown = (props) => { 8 | const ref = useRef() 9 | const inputRef = useRef() 10 | const appContext = useContext(AppContext) 11 | 12 | const [results, setResults] = useState([]) 13 | const [query, setQuery] = useState('') 14 | const [typingTimer, setTypingTimer] = useState(null) 15 | const [isFetching, setIsFetching] = useState(false) 16 | const [noResults, setNoResults] = useState(false) 17 | const [pageNum, setPageNum] = useState(1) 18 | 19 | const [activeIndex, setActiveIndex] = useState(-1) 20 | 21 | const handleEnter = () => { 22 | 23 | // props.closeDropdown() 24 | } 25 | 26 | const handleKeyDown = (e) => { 27 | if (e.key === 'Escape') { 28 | props.closeDropdown() 29 | } 30 | 31 | if (e.key === 'Enter') { 32 | e.preventDefault() 33 | 34 | handleEnter() 35 | } 36 | } 37 | 38 | const fetchAutosuggest = () => { 39 | if (query.length < 2) return 40 | 41 | setIsFetching(true) 42 | 43 | fetch(`/api/v1/search?q=${query}&is_as=true&page=${pageNum}`, { 44 | method: 'GET', 45 | headers: { 46 | 'Content-type': 'application/json' 47 | } 48 | }) 49 | .then(res => { 50 | if (!res.ok) { 51 | if (res.status === 401) { 52 | this.setState({ accessDenied: true }) 53 | } 54 | throw new Error(res.status) 55 | } 56 | else return res.json() 57 | }) 58 | .then(data => { 59 | if (data.results.length === 0) { 60 | setNoResults(true) 61 | setIsFetching(false) 62 | return 63 | } 64 | 65 | if (pageNum === 1) { 66 | setResults(data.results) 67 | } 68 | else { 69 | setResults([...results, ...data.results]) 70 | } 71 | 72 | setIsFetching(false) 73 | setPageNum(pageNum+1) 74 | }) 75 | .catch(error => { 76 | console.log('error: ' + error) 77 | }) 78 | } 79 | 80 | const handleInputKeyUp = (e) => { 81 | clearTimeout(typingTimer) 82 | const timer = setTimeout(() => { fetchAutosuggest() }, 750) 83 | setTypingTimer(timer) 84 | 85 | // go up and down with arrow keys 86 | if (e.key === 'ArrowDown') { 87 | setActiveIndex(activeIndex+1) 88 | } 89 | 90 | if (e.key === 'ArrowUp') { 91 | setActiveIndex(activeIndex-1) 92 | } 93 | } 94 | 95 | const handleQueryChange = (e) => { 96 | setQuery(e.target.value) 97 | 98 | setPageNum(1) 99 | setResults([]) 100 | setActiveIndex(-1) 101 | } 102 | 103 | const handleActivityResultClick = (e, activity) => { 104 | props.addNewNode(activity) 105 | props.closeDropdown() 106 | } 107 | 108 | const handleSpaceResultClick = (e, space) => { 109 | props.addNewNode(space) 110 | props.closeDropdown() 111 | } 112 | 113 | const handleResultMouseOver = (e, index) => { 114 | setActiveIndex(index) 115 | } 116 | 117 | const handleCreateNewSpaceClick = () => { 118 | const body = { 119 | title: query, 120 | parent_id: props.space ? props.space.id : null 121 | } 122 | 123 | fetch(`/api/v1/spaces/`, { 124 | method: 'POST', 125 | headers: { 126 | 'Content-type': 'application/json' 127 | }, 128 | body: JSON.stringify(body) 129 | }) 130 | .then(res => { 131 | if (!res.ok) throw new Error(res.status) 132 | else return res.json() 133 | }) 134 | .then(space => { 135 | // success 136 | // linkToSpace(space) 137 | props.closeDropdown() 138 | }) 139 | .catch(error => { 140 | console.log('error: ' + error) 141 | }) 142 | } 143 | 144 | const handleScroll = (e) => { 145 | const remainingScrollHeight = e.target.scrollHeight - e.target.scrollTop 146 | const bottom = (remainingScrollHeight - e.target.clientHeight) < 50 147 | 148 | if (!bottom || isFetching || results.length === 0) return 149 | 150 | fetchAutosuggest() 151 | } 152 | 153 | const handleResultKeyUp = (e, obj, objType) => { 154 | if (e.key === 'ArrowDown') { 155 | e.preventDefault() 156 | setActiveIndex(activeIndex+1) 157 | } 158 | 159 | if (e.key === 'ArrowUp') { 160 | e.preventDefault() 161 | 162 | if (activeIndex === 0) { 163 | inputRef.current.focus() 164 | } 165 | 166 | setActiveIndex(activeIndex-1) 167 | } 168 | 169 | if (e.key === 'Enter') { 170 | e.preventDefault() 171 | 172 | if (objType === 'Activity') { 173 | handleActivityResultClick(e, obj) 174 | } 175 | else { 176 | handleSpaceResultClick(e, obj) 177 | } 178 | } 179 | } 180 | 181 | const buildResults = () => { 182 | if (query.length < 2) { 183 | 184 | return '' 185 | } 186 | 187 | const asResults = results.map((result, index) => { 188 | const buildContent = { 189 | Activity: () => { 190 | return( 191 | 200 | ) 201 | }, 202 | Space: () => { 203 | return( 204 | 213 | ) 214 | } 215 | }[result.content_type] 216 | 217 | return buildContent() 218 | }) 219 | 220 | return( 221 |
224 |
225 | Matching content 226 |
227 |
228 | { asResults } 229 |
230 |
231 | ) 232 | } 233 | 234 | const buildLoadingGIF = () => { 235 | if (query.length < 2) return '' 236 | 237 | let message = '' 238 | if (results.length === 0 && isFetching) { 239 | message =
240 | Tip: you can link to (or create) a page by just hitting 'Enter' now 241 |
242 | } 243 | 244 | return( 245 |
246 | { message } 247 | 253 |
254 | ) 255 | } 256 | 257 | const buildLoadingMoreIndicator = () => { 258 | if (isFetching) { 259 | return buildLoadingGIF() 260 | } 261 | 262 | if (noResults === true) { 263 | return('') 264 | } 265 | } 266 | 267 | const buildCreateNewSpace = () => { 268 | if (query.length < 2) return '' 269 | 270 | return( 271 |
276 | Create and link to page "{ query }" 277 |
278 | ) 279 | } 280 | 281 | const buildNewSearch = () => { 282 | if (query.length < 2) return '' 283 | 284 | const newTabUrl = `/?q=${query}` 285 | 286 | return( 287 | 288 |
289 | Search in new tab "{ query }" 290 |
291 |
292 | ) 293 | } 294 | 295 | return( 296 | // left: `${dropdownLeft}px` 297 | // top: query.length < 2 ? `${dropdownTop}px` : `${dropdownTop+300}px`, 298 |
307 |
308 | setActiveIndex(-1)} 318 | style={{paddingLeft: '0.5em', paddingRight: '0.5em', maxWidth: '90%'}} 319 | /> 320 |
321 | { buildResults() } 322 | { buildLoadingMoreIndicator() } 323 |
324 | ) 325 | // { buildCreateNewSpace() } 326 | } 327 | -------------------------------------------------------------------------------- /src/components/PageTabs.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react' 2 | import queryString from 'query-string' 3 | 4 | export const PageTabs = (props) => { 5 | const buildTabContent = () => { 6 | return '' 7 | } 8 | 9 | const buildTab = (tab) => { 10 | const icon = tab.iconUrl ?
: '' 11 | 12 | return( 13 |
props.updateCurrTab(tab.name)} 17 | title={tab.description} 18 | > 19 | { icon } 20 |
{ tab.displayName }
21 |
22 | ) 23 | } 24 | 25 | const buildTabs = () => { 26 | return props.tabs.map(tab => { 27 | return buildTab(tab) 28 | }) 29 | } 30 | 31 | return( 32 |
33 |
34 | { buildTabs() } 35 |
36 |
37 | ) 38 | //
props.updateCurrTab('fields')} 42 | // > 43 | // Properties 44 | //
45 | //
46 | } -------------------------------------------------------------------------------- /src/components/ResultFilters.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | export const ResultFilters = (props) => { 4 | const [showFilters, setShowFilters] = useState(false) 5 | 6 | const buildFilterBtns = () => { 7 | if (!showFilters) return '' 8 | 9 | return( 10 |
11 |
12 | Time 13 |
14 |
15 | ) 16 | } 17 | 18 | return( 19 |
20 |
21 |
22 | 28 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | ) 38 | //
setShowFilters(!showFilters)} 41 | // > 42 | // filter 43 | //
44 | // { buildFilterBtns() } 45 | } -------------------------------------------------------------------------------- /src/components/SpaceASResult.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react" 2 | 3 | export const SpaceASResult = (props) => { 4 | const ref = useRef() 5 | 6 | const buildTextBody = () => { 7 | if (!props.space.text_body) return '' 8 | 9 | return( 10 |
11 | { props.space.text_body.slice(0,300) } 12 |
13 | ) 14 | } 15 | 16 | if (props.index === props.activeIndex) { 17 | if (ref.current) { 18 | ref.current.focus() 19 | } 20 | } 21 | 22 | return ( 23 |
props.handleClick(e, props.space)} 29 | onMouseOver={(e) => props.handleResultMouseOver(e, props.index)} 30 | onKeyUp={(e) => props.handleResultKeyUp(e, props.space, 'Space')} 31 | > 32 |
{ props.space.title || 'Untitled' }
33 | 34 | { buildTextBody() } 35 |
36 | ) 37 | } -------------------------------------------------------------------------------- /src/components/SpaceModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { BlockEditorSlate } from './BlockEditorSlate' 3 | import AppContext from './AppContext' 4 | import { IRuminWindow } from "./Utils/IRuminWindow"; 5 | 6 | declare var window: IRuminWindow; 7 | 8 | export interface ISpaceModelState { 9 | isDataFetched: boolean; 10 | isFetching: boolean; 11 | jsonBody: any; 12 | } 13 | 14 | export class SpaceModal extends React.Component <{}, ISpaceModelState> { 15 | static contextType = AppContext 16 | 17 | constructor(props: any, context: any) { 18 | super(props, context); 19 | 20 | this.handleTitleChange = this.handleTitleChange.bind(this) 21 | this.handleTitleBlur = this.handleTitleBlur.bind(this) 22 | 23 | this.state = { 24 | // listItems: this.initDemoListItems(), 25 | isDataFetched: false, 26 | jsonBody: [], 27 | isFetching: null 28 | }; 29 | } 30 | // const appContext = useContext(AppContext) 31 | 32 | // const [isFetching, setIsFetching] = useState(false) 33 | // const [jsonBody, setJsonBody] = useState() 34 | 35 | componentDidMount() { 36 | if (this.context.modalSpaceId && !this.context.modalSpace && !this.state.isFetching) { 37 | console.log('TODO - fetch space') 38 | 39 | this.setState({ isFetching: true }) 40 | 41 | fetch(`/api/v1/spaces/${this.context.modalSpaceId}/`, { 42 | method: 'GET', 43 | headers: { 44 | 'Content-type': 'application/json' 45 | } 46 | }) 47 | .then(res => { 48 | if (!res.ok) { 49 | // if (res.status === 401) { 50 | // this.setState({ accessDenied: true }) 51 | // } 52 | throw new Error(`${res.status}`) 53 | } 54 | else return res.json() 55 | }) 56 | .then(data => { 57 | console.log('fetched data for modalSpace', data) 58 | this.context.updateModalSpace(data) 59 | 60 | this.setState({ isFetching: false, isDataFetched: true }) 61 | }) 62 | .catch(error => { 63 | 64 | }) 65 | } 66 | 67 | } 68 | 69 | // componentDidUpdate(prevProps, prevState) { 70 | // console.log('nextProps', nextProps, 'nextState', nextState) 71 | // } 72 | 73 | // shouldComponentUpdate(nextProps, nextState) { 74 | // if (!nextState.jsonBody) return false 75 | 76 | // return true 77 | // } 78 | 79 | 80 | // useEffect(() => { 81 | // }) 82 | 83 | userIsAuthenticated () { 84 | return window.django.user.is_authenticated 85 | } 86 | 87 | userIsAuthor () { 88 | return this.userIsAuthenticated() && window.django.user.id.toString() == this.context.modalSpace.user 89 | } 90 | 91 | handleTitleChange (e: any) { 92 | this.context.updateModalSpace({ 93 | ...this.context.modalSpace, 94 | title: e.target.value 95 | }) 96 | } 97 | 98 | handleTitleBlur (e: any) { 99 | console.log('this.context.modalSpace', this.context.modalSpace, this.context) 100 | console.log('this.context.modalSpace.title', this.context.modalSpace.title) 101 | 102 | fetch(`/api/v1/spaces/${this.context.modalSpaceId}/`, { 103 | method: 'PATCH', 104 | headers: { 105 | 'Content-type': 'application/json' 106 | }, 107 | body: JSON.stringify({ title: this.context.modalSpace.title }) 108 | }) 109 | .then(res => { 110 | if (!res.ok) throw new Error(`${res.status}`) 111 | else return res.json() 112 | }) 113 | .then(data => { 114 | console.log('saved title!', data) 115 | }) 116 | .catch(error => { 117 | console.log('error: ' + error) 118 | }) 119 | } 120 | 121 | buildSpaceTitle () { 122 | if (!this.context.modalSpace) return '' 123 | 124 | return( 125 | 134 | ) 135 | } 136 | 137 | buildSpaceContent () { 138 | if (!this.context.modalSpace) return '' 139 | 140 | return( 141 |
148 | 154 |
155 | ) 156 | } 157 | //
{ appContext.modalSpace }
158 | render() { 159 | return( 160 |
161 | { this.buildSpaceTitle() } 162 | { this.buildSpaceContent() } 163 |
164 | ) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/components/SpaceOverviewSection.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { BlockEditorSlate } from "./BlockEditorSlate" 3 | 4 | export const SpaceOverviewSection = (props) => { 5 | const [showOverview, setShowOverview] = useState(true) 6 | 7 | useEffect(() => { 8 | if (props.isNewSpace) return 9 | }) 10 | 11 | const uniqueById = (content) => { 12 | return [...content.reduce((a,c)=>{ 13 | a.set(c.id, c); 14 | return a; 15 | }, new Map()).values()]; 16 | } 17 | 18 | const buildHideOverviewBtn = () => { 19 | if (showOverview) { 20 | return( 21 | setShowOverview(false)} 25 | > 26 | Hide 27 | 28 | ) 29 | } 30 | 31 | return( 32 | setShowOverview(true)} 36 | > 37 | Show 38 | 39 | ) 40 | } 41 | 42 | const buildLoadingGIF = () => { 43 | return( 44 |
45 | 51 |
52 | ) 53 | } 54 | 55 | const buildSpaceEditor = () => { 56 | return( 57 |
58 | 71 |
72 | ) 73 | } 74 | 75 | const buildOverviewSection = () => { 76 | //
77 | // Overview 78 | // { buildHideOverviewBtn() } 79 | //
80 | return( 81 |
82 |
87 | { buildSpaceEditor() } 88 |
89 |
90 | ) 91 | } 92 | 93 | if (props.accessDenied) { 94 | return( 95 |
96 |

You do not have the permission to access this page.

97 |

98 | Trying contacting the author to request for access. 99 |

100 |
101 | ) 102 | } 103 | 104 | if (!props.isNewSpace && !props.isDataFetched) return buildLoadingGIF() 105 | 106 | if (!props.userIsAuthor && !props.isNewSpace) { 107 | return( 108 |
109 | { buildSpaceEditor() } 110 |
111 | ) 112 | } 113 | 114 | return( 115 |
116 | { buildOverviewSection() } 117 |
118 | ) 119 | } -------------------------------------------------------------------------------- /src/components/SuggestionCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react' 2 | 3 | export const SuggestionCard = (props) => { 4 | // 1 is accept, -1 is dismiss 5 | const updateAccepted = (accepted) => { 6 | fetch(`/api/v1/suggestions/${props.suggestion.id}/`, { 7 | method: 'PATCH', 8 | headers: { 9 | 'Content-type': 'application/json' 10 | }, 11 | body: JSON.stringify({ accepted: accepted }) 12 | }) 13 | .then(res => { 14 | if (!res.ok) throw new Error(res.status) 15 | else return res.json() 16 | }) 17 | .then(data => { 18 | props.updateSuggestions(props.suggestion, accepted) 19 | }) 20 | .catch(error => { 21 | console.log('error: ' + error) 22 | }) 23 | } 24 | 25 | const handleAddClick = () => { 26 | updateAccepted(1) 27 | } 28 | 29 | const handleDismissClick = () => { 30 | updateAccepted(-1) 31 | } 32 | 33 | const buildAcceptControls = () => { 34 | if (props.suggestion.accepted) return '' 35 | 36 | return( 37 |
38 |
42 | Add as sub-page 43 |
44 |
48 | Dismiss 49 |
50 |
51 | ) 52 | } 53 | 54 | if (!props.suggestion.custom_fields) { 55 | return( 56 |
57 | { props.suggestion.short_description } 58 | 59 | { buildAcceptControls() } 60 |
61 | ) 62 | } 63 | 64 | const buildTitle = () => { 65 | if (props.suggestion.suggested_obj) { 66 | const content = props.suggestion.suggested_obj 67 | const url = props.suggestion.suggested_obj.content_type === 'Space' ? `/spaces/${props.suggestion.suggested_obj.id}` : `/activities/${props.suggestion.suggested_obj.id}` 68 | 69 | return( 70 |
71 |
72 |

73 | { content.title } 74 |

75 |
76 |
77 | ) 78 | } 79 | 80 | const title = props.suggestion.custom_fields.entity_title || '' 81 | const subtitle = props.suggestion.custom_fields.entity_subtitle || '' 82 | 83 | return( 84 |
85 |
86 |

{ title }

87 |
88 |
89 | 90 | { subtitle } 91 | 92 |
93 |
94 | ) 95 | } 96 | 97 | const buildDescription = () => { 98 | if (props.suggestion.suggested_obj) { 99 | const content = props.suggestion.suggested_obj 100 | 101 | return( 102 |
103 | { (content.text_body && content.text_body.slice(0, 300)) || '' } 104 |
105 | ) 106 | } 107 | 108 | return( 109 |
110 | { props.suggestion.short_description } 111 |
112 | ) 113 | } 114 | 115 | const buildFields = () => { 116 | const excludedFields = ['entity_title', 'entity_subtitle', 'entity_description', 'people_also_search_for', 'related_people', 'wikipedia_url', 'twitter_url', 'youtube_url', 'official site_url', 'official_site_url', 'linkedin_url'] 117 | const fieldNames = Object.keys(props.suggestion.custom_fields).filter(k => !excludedFields.includes(k)) 118 | 119 | const fields = fieldNames.map(fieldName => { 120 | const val = props.suggestion.custom_fields[fieldName] 121 | 122 | if (val.split(':').length > 1) { 123 | const [key, value] = val.split(':') 124 | 125 | return( 126 |
127 | { key }: 128 | { value } 129 |
130 | ) 131 | } 132 | 133 | return( 134 |
135 | { val } 136 |
137 | ) 138 | }) 139 | 140 | return( 141 |
142 | { fields } 143 |
144 | ) 145 | } 146 | 147 | return( 148 |
149 | { buildTitle() } 150 | 151 | { buildDescription() } 152 | 153 | { buildFields() } 154 | 155 | { buildAcceptControls() } 156 |
157 | ) 158 | } 159 | -------------------------------------------------------------------------------- /src/components/SuggestionsSection.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { SuggestionCard } from './SuggestionCard' 3 | 4 | export const SuggestionsSection = (props) => { 5 | const [contentSuggestions, setContentSuggestions] = useState([]) 6 | const [hasFetchedContentSuggestions, setHasFetchedContentSuggestions] = useState(false) 7 | const [isFetchingContentSuggestions, setIsFetchingContentSuggestions] = useState(false) 8 | 9 | const fetchContentSuggestions = () => { 10 | setIsFetchingContentSuggestions(true) 11 | 12 | fetch(`/api/v1/spaces/${props.space.id}/suggestions`, { 13 | method: 'GET', 14 | headers: { 15 | 'Content-type': 'application/json' 16 | } 17 | }) 18 | .then(res => { 19 | if (!res.ok) { 20 | setHasFetchedContentSuggestions(true) 21 | setIsFetchingContentSuggestions(false) 22 | throw new Error(res.status) 23 | } 24 | else return res.json() 25 | }) 26 | .then(data => { 27 | setContentSuggestions(data) 28 | setHasFetchedContentSuggestions(true) 29 | setIsFetchingContentSuggestions(false) 30 | }) 31 | .catch(error => { 32 | console.log('error: ' + error) 33 | }) 34 | } 35 | 36 | useEffect(() => { 37 | if (props.space && !hasFetchedContentSuggestions && !isFetchingContentSuggestions) { 38 | fetchContentSuggestions() 39 | } 40 | }) 41 | 42 | useEffect(() => { 43 | // fetch new suggestions, on title change 44 | fetch(`/api/v1/spaces/${props.space.id}/suggestions`, { 45 | method: 'GET', 46 | headers: { 47 | 'Content-type': 'application/json' 48 | } 49 | }) 50 | .then(res => { 51 | if (!res.ok) { 52 | throw new Error(res.status) 53 | } 54 | else return res.json() 55 | }) 56 | .then(data => { 57 | setContentSuggestions(uniqueById([...contentSuggestions, ...data])) 58 | }) 59 | .catch(error => { 60 | console.log('error: ' + error) 61 | }) 62 | }, [props.space.title]) 63 | 64 | const uniqueById = (content) => { 65 | return [...content.reduce((a,c)=>{ 66 | a.set(c.id, c); 67 | return a; 68 | }, new Map()).values()]; 69 | } 70 | 71 | const buildLoadingGIF = () => { 72 | return( 73 |
74 | 80 |
81 | ) 82 | } 83 | 84 | const updateSuggestions = (suggestion, accepted) => { 85 | const updated = contentSuggestions.map(s => { 86 | if (s.id !== suggestion.id) return s 87 | 88 | const updatedSuggestion = { 89 | ...s, 90 | accepted: accepted 91 | } 92 | return updatedSuggestion 93 | }) 94 | setContentSuggestions(updated) 95 | 96 | // if accepted, add to the space.collection_data on client side 97 | if (accepted === 1 && suggestion.suggested_obj) { 98 | let newSpace = Object.assign({}, props.space) 99 | newSpace.collection_data = [...newSpace.collection_data, suggestion.suggested_obj] 100 | props.updateSpace(newSpace) 101 | } 102 | } 103 | 104 | const getPendingSuggestions = () => { 105 | return contentSuggestions.filter(s => s.accepted === 0) 106 | } 107 | 108 | const buildPendingSuggestions = () => { 109 | const suggestions = getPendingSuggestions() 110 | if (suggestions.length === 0) { 111 | return( 112 |
113 |

114 | No more suggestions for this page for now. 115 |

116 |
117 | ) 118 | } 119 | 120 | return suggestions.map(suggestion => { 121 | return( 122 | 126 | ) 127 | }) 128 | } 129 | 130 | const buildPendingSuggestionsSection = () => { 131 | if (isFetchingContentSuggestions) { 132 | return( 133 |
134 | 💡 Checking for AI suggestions... 135 |
136 | ) 137 | } 138 | // if (getPendingSuggestions().length === 0) return '' 139 | 140 | //

💡 AI suggestions

141 | return( 142 |
143 | { buildPendingSuggestions() } 144 |
145 | ) 146 | } 147 | 148 | 149 | return( 150 |
151 | 152 | { buildPendingSuggestionsSection() } 153 |
154 | ) 155 | } -------------------------------------------------------------------------------- /src/components/Utils/IRuminWindow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend the window object with known properties we put into the global namespace. 3 | * 4 | * @interface IRuminWindow 5 | */ 6 | 7 | export interface IRuminWindow { 8 | /** 9 | * django object 10 | */ 11 | django: any; 12 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import App from "./components/MainApp/App"; 2 | import { render } from "react-dom"; 3 | import * as React from "react"; 4 | 5 | const container = document.getElementById("rumin_body"); 6 | render(, container); -------------------------------------------------------------------------------- /src/utils/date_utils.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | const startWithLowerCase = (str) => { 4 | return str.slice(0, 1).toLowerCase() + str.slice(1,) 5 | } 6 | 7 | export const friendlyDateStr = (date) => { 8 | const momentDate = moment(date); 9 | return momentDate.calendar(null,{ 10 | lastDay : '[Yesterday]', 11 | sameDay : '[Today]', 12 | nextDay : '[Tomorrow]', 13 | lastWeek : '[last] dddd', 14 | nextWeek : 'dddd', 15 | sameElse : 'L' 16 | }) 17 | } 18 | 19 | export const friendlyDateTimeStr = (date) => { 20 | const momentDate = moment(date); 21 | const str = momentDate.calendar() 22 | return startWithLowerCase(str) 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/params_utils.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-frontend/a1f5394fea71b7cab5e2217f097521816acdaee5/src/utils/params_utils.js -------------------------------------------------------------------------------- /static/frontend/Block.css: -------------------------------------------------------------------------------- 1 | [contenteditable]:focus { 2 | outline: 0px solid transparent; 3 | } 4 | 5 | .block { 6 | } 7 | 8 | .block.underline { 9 | border-bottom: 2px solid #1DA1F2; 10 | } 11 | 12 | .bullet { 13 | width: 18px; 14 | position: absolute; 15 | } 16 | 17 | .block-content { 18 | padding-left: 24px; 19 | } 20 | 21 | .children { 22 | margin-left: 8px; 23 | padding-left: 24px; 24 | border-left: 1px solid rgb(236, 238, 240); 25 | } 26 | 27 | .document { 28 | max-width: 820px; 29 | margin-left: auto; 30 | margin-right: auto; 31 | } 32 | 33 | .header { 34 | height: 46px; 35 | position: fixed; 36 | border-bottom: 1px solid rgb(236, 238, 240); 37 | } 38 | 39 | .page-content { 40 | margin-top: 72px; 41 | /*padding-top;*/ 42 | } 43 | -------------------------------------------------------------------------------- /static/frontend/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2015 Jed Watson. 9 | Based on code that is Copyright 2013-2015, Facebook, Inc. 10 | All rights reserved. 11 | */ 12 | 13 | /*! 14 | Copyright (c) 2017 Jed Watson. 15 | Licensed under the MIT License (MIT), see 16 | http://jedwatson.github.io/classnames 17 | */ 18 | 19 | /*! 20 | * Adapted from jQuery UI core 21 | * 22 | * http://jqueryui.com 23 | * 24 | * Copyright 2014 jQuery Foundation and other contributors 25 | * Released under the MIT license. 26 | * http://jquery.org/license 27 | * 28 | * http://api.jqueryui.com/category/ui-core/ 29 | */ 30 | 31 | /*! 32 | * escape-html 33 | * Copyright(c) 2012-2013 TJ Holowaychuk 34 | * Copyright(c) 2015 Andreas Lubbe 35 | * Copyright(c) 2015 Tiancheng "Timothy" Gu 36 | * MIT Licensed 37 | */ 38 | 39 | /*! 40 | * is-plain-object 41 | * 42 | * Copyright (c) 2014-2017, Jon Schlinkert. 43 | * Released under the MIT License. 44 | */ 45 | 46 | /*! 47 | * isobject 48 | * 49 | * Copyright (c) 2014-2017, Jon Schlinkert. 50 | * Released under the MIT License. 51 | */ 52 | 53 | /*! ***************************************************************************** 54 | Copyright (c) Microsoft Corporation. All rights reserved. 55 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 56 | this file except in compliance with the License. You may obtain a copy of the 57 | License at http://www.apache.org/licenses/LICENSE-2.0 58 | 59 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 60 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 61 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 62 | MERCHANTABLITY OR NON-INFRINGEMENT. 63 | 64 | See the Apache Version 2.0 License for specific language governing permissions 65 | and limitations under the License. 66 | ***************************************************************************** */ 67 | 68 | /*! https://mths.be/esrever v0.2.0 by @mathias */ 69 | 70 | /** 71 | * 72 | * Polynomial.js 73 | * 74 | * copyright 2002, 2013 Kevin Lindsey 75 | * 76 | * contribution {@link http://github.com/Quazistax/kld-polynomial} 77 | * @copyright 2015 Robert Benko (Quazistax) 78 | * @license MIT 79 | */ 80 | 81 | /** 82 | * 83 | * Intersection.js 84 | * 85 | * copyright 2002, 2013 Kevin Lindsey 86 | * 87 | * contribution {@link http://github.com/Quazistax/kld-intersections} 88 | * @copyright 2015 Robert Benko (Quazistax) 89 | * @license MIT 90 | */ 91 | 92 | /** @license React v0.18.0 93 | * scheduler.production.min.js 94 | * 95 | * Copyright (c) Facebook, Inc. and its affiliates. 96 | * 97 | * This source code is licensed under the MIT license found in the 98 | * LICENSE file in the root directory of this source tree. 99 | */ 100 | 101 | /** @license React v0.19.1 102 | * scheduler.production.min.js 103 | * 104 | * Copyright (c) Facebook, Inc. and its affiliates. 105 | * 106 | * This source code is licensed under the MIT license found in the 107 | * LICENSE file in the root directory of this source tree. 108 | */ 109 | 110 | /** @license React v0.25.1 111 | * react-reconciler.production.min.js 112 | * 113 | * Copyright (c) Facebook, Inc. and its affiliates. 114 | * 115 | * This source code is licensed under the MIT license found in the 116 | * LICENSE file in the root directory of this source tree. 117 | */ 118 | 119 | /** @license React v16.12.0 120 | * react-dom.production.min.js 121 | * 122 | * Copyright (c) Facebook, Inc. and its affiliates. 123 | * 124 | * This source code is licensed under the MIT license found in the 125 | * LICENSE file in the root directory of this source tree. 126 | */ 127 | 128 | /** @license React v16.12.0 129 | * react-is.production.min.js 130 | * 131 | * Copyright (c) Facebook, Inc. and its affiliates. 132 | * 133 | * This source code is licensed under the MIT license found in the 134 | * LICENSE file in the root directory of this source tree. 135 | */ 136 | 137 | /** @license React v16.12.0 138 | * react.production.min.js 139 | * 140 | * Copyright (c) Facebook, Inc. and its affiliates. 141 | * 142 | * This source code is licensed under the MIT license found in the 143 | * LICENSE file in the root directory of this source tree. 144 | */ 145 | 146 | /**! 147 | * @fileOverview Kickass library to create and place poppers near their reference elements. 148 | * @version 1.16.1 149 | * @license 150 | * Copyright (c) 2016 Federico Zivolo and contributors 151 | * 152 | * Permission is hereby granted, free of charge, to any person obtaining a copy 153 | * of this software and associated documentation files (the "Software"), to deal 154 | * in the Software without restriction, including without limitation the rights 155 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | * copies of the Software, and to permit persons to whom the Software is 157 | * furnished to do so, subject to the following conditions: 158 | * 159 | * The above copyright notice and this permission notice shall be included in all 160 | * copies or substantial portions of the Software. 161 | * 162 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 168 | * SOFTWARE. 169 | */ 170 | -------------------------------------------------------------------------------- /templates/frontend/about_validate.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Idea Validated 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 63 | {% load user_agents %} 64 | 65 | 66 | {% include 'frontend/vaas_header.html' %} 67 | 68 |
69 |
70 |
71 |
72 |

Hi there 👋

73 |

My name is John.

74 | 75 |
76 | 77 |
78 | 79 |
80 |

I am a maker, learner, and former Product Manager.

81 | 82 |

Building a successful product is hard. By default, the world is too busy to care about your idea. It is sad to see teams wasting months chasing after ideas based on false assumptions that could have been prevented.

83 |

Life is too short to build products no one cares about.

84 | 85 |

I have worked with many companies, big and small, on validating and building their product ideas. Along the way, I have learned many lessons the hard way.

86 | 87 |

As a founder, creative person, or a high-functioning product team, naturally you have more ideas than you will ever have time to build.

88 | 89 |

Let us help you vet them, so you can focusing on the most promising ideas.

90 |
91 |
92 |
93 | 94 | 95 | 96 |
97 |
98 |

Let's do this.

99 | 100 | 101 |
102 |
103 | 104 | 109 |
110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | {% if not debug %} 118 | 119 | 120 | 127 | {% endif %} 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /templates/frontend/activity_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 |
6 | 7 |
8 | 9 | {% load static %} 10 | 11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /templates/frontend/coding_lookup_article.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ space.title }} | Top Code Examples 9 | 10 | {% if description %} 11 | 12 | {% endif %} 13 | 14 | {% if keywords %} 15 | 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 96 | {% load user_agents %} 97 | 98 | 99 | {% include 'frontend/coding_utility_header.html' %} 100 | 101 |
102 |
103 |
104 |

{{ space.title }}

105 |
106 | 107 |
108 | {{ html_body | safe }} 109 |
110 | 111 |
112 |
113 | 114 |
115 |

Get coding tips in your inbox

116 |

Leave your email for weekly coding tips and resources

117 |
118 | 119 | 120 |
121 |
122 | 123 | 124 | 125 | 128 | {% if not debug %} 129 | 130 | 131 | 138 | {% endif %} 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /templates/frontend/coding_utility.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Top Code Examples 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 92 | {% load user_agents %} 93 | 94 | 95 | {% include 'frontend/coding_utility_header.html' %} 96 | 97 |
98 |
99 |
100 |
101 |

Programming Reference and Code Examples

102 | 103 |

Quick access for programming knowledge and syntax lookup

104 | 105 | 110 |
111 |
112 |
113 | 114 | 146 | 147 |
148 |

Get coding tips in your inbox

149 |

Leave your email for weekly coding tips and resources

150 | 151 |
152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 | {% if not debug %} 160 | 161 | 162 | 169 | {% endif %} 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /templates/frontend/coding_utility_header.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /templates/frontend/demo_graph.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 |
6 | 7 |
8 | 9 | {% load static %} 10 | 11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /templates/frontend/demo_grid_board.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rumin 7 | 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | {% include 'collections_pane.html' %} 14 | 15 |
16 | {% include 'search_section.html' %} 17 | 18 |
19 |

Growth strategy (sandbox)

20 | 21 |
22 |
23 |
24 |

Some thought that I have

25 |
26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 27 |
28 |
29 |
30 | 31 |
32 |
33 |

Some thought that I have

34 |
35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 36 |
37 |
38 |
39 | 40 |
41 |
42 |

Some thought that I have

43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 45 |
46 |
47 |
48 | 49 |
50 |
51 |

Some thought that I have

52 |
53 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 54 |
55 |
56 |
57 |
58 | 59 | 104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /templates/frontend/demo_log_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rumin 7 | 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /templates/frontend/demo_mood_board.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rumin 7 | 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | {% include 'collections_pane.html' %} 14 | 15 |
16 | {% include 'search_section.html' %} 17 | 18 |
19 |

Growth strategy (sandbox)

20 | 21 |
22 |
23 |
24 |

Some thought that I have

25 |
26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 27 |
28 |
29 |
30 | 31 |
32 |
33 |

Some thought that I have

34 |
35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 36 |
37 |
38 |
39 | 40 |
41 |
42 |

Some thought that I have

43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 45 |
46 |
47 |
48 | 49 |
50 |
51 |

Some thought that I have

52 |
53 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 54 |
55 |
56 |
57 |
58 | 59 | 104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /templates/frontend/demo_writer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rumin 7 | 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | 14 | {% include 'collections_pane.html' %} 15 | 16 |
17 | {% include 'search_section.html' %} 18 | 19 |
20 |

Growth strategy (sandbox)

21 | 22 |
23 | 24 |
25 | 26 | 71 |
72 |
73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /templates/frontend/explorer.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'base.html' %} 8 | 9 | {% block content %} 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | {% load static %} 19 | 20 | 21 | {% if not debug %} 22 | 49 | {% endif %} 50 | 51 | 52 | {% if not debug %} 53 | 63 | {% endif %} 64 | 65 | 66 | {% if debug %} 67 | 99 | {% else %} 100 | 132 | {% endif %} 133 | 134 | {% endblock content %} 135 | -------------------------------------------------------------------------------- /templates/frontend/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | {% load static %} 14 | 15 | 16 | {% if not debug %} 17 | 44 | 45 | {% endif %} 46 | 47 | 48 | 49 | {% if not debug %} 50 | 60 | {% endif %} 61 | 62 | 63 | {% if debug %} 64 | 96 | {% else %} 97 | 129 | {% endif %} 130 | 131 | 132 | 133 | {% endblock content %} 134 | -------------------------------------------------------------------------------- /templates/frontend/ka_article.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | {{ space.title }} | Knowledge Artist 8 | 9 | {% if description %} 10 | 11 | {% endif %} 12 | 13 | {% if keywords %} 14 | 15 | {% endif %} 16 | 17 | {% if space.short_url %} 18 | 19 | {% else %} 20 | 21 | {% endif %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 61 | {% load user_agents %} 62 | 63 | 64 | {% include 'frontend/ka_header.html' %} 65 | 66 | 67 | 69 | 70 |
71 |
72 |
73 |

{{ space.title }}

74 | 75 |
76 | {{ space.created_at|date:"M d Y" }} 77 |
78 |
79 | 80 |
81 | {{ html_body | safe }} 82 | 83 | 84 | {% if show_newsletter_prompt %} 85 |
86 |

Every two weeks, I send out a newsletter where I share my creative work, learnings on product, life in a digital age, automation, and other cool topics.

87 |

Enter your email below to subscribe.

88 | 89 |
90 | 91 |
92 | {% else %} 93 |
94 | 95 | {% endif %} 96 |
97 |
98 | 99 | {% include 'frontend/ka_share.html' %} 100 | 101 | {% if not is_newsletter %} 102 | {% include 'frontend/ka_more_articles.html' %} 103 | {% endif %} 104 |
105 | 106 |
107 |
108 | 109 |
110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | {% if not debug %} 118 | 119 | 120 | 127 | {% endif %} 128 | 129 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /templates/frontend/ka_articles.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Knowledge Artist 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 76 | 77 | {% load user_agents %} 78 | 79 | {% include 'frontend/ka_articles_data.html' %} 80 | 81 | {% include 'frontend/ka_header.html' %} 82 | 83 | 84 |
85 |
86 |

Essays

87 | 88 |
89 |
Categories
90 | 91 |
92 |
All
93 |
Knowledge
94 |
Automation
95 |
Entrepreneurship
96 |
Life
97 |
Food
98 |
The Future
99 |
Music
100 |
Language learning
101 | 102 |
Productivity
103 |
104 |
105 | 106 |
107 | 108 |
109 | 114 | 115 |
116 |
117 | 118 | 141 |
142 |
143 | 144 | 145 | 146 | 147 | {% if not debug %} 148 | 149 | 150 | 157 | {% endif %} 158 | 159 | 233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /templates/frontend/ka_articles_data.html: -------------------------------------------------------------------------------- 1 | 155 | -------------------------------------------------------------------------------- /templates/frontend/ka_header.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 57 | -------------------------------------------------------------------------------- /templates/frontend/ka_more_articles.html: -------------------------------------------------------------------------------- 1 | {% include 'frontend/ka_articles_data.html' %} 2 | 3 | 8 | 9 |
10 |
11 | 17 | 18 | 24 |
25 |
26 | 27 | 54 | -------------------------------------------------------------------------------- /templates/frontend/ka_share.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 20 | -------------------------------------------------------------------------------- /templates/frontend/knowledge_artist.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Knowledge Artist 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 56 | {% load user_agents %} 57 | 58 | 59 | 60 | {% include 'frontend/ka_header.html' %} 61 | 62 |
63 |
64 |
65 | 66 |
67 | 68 |

Hi, I'm John Yeung

69 | 70 |
71 |

Welcome to my site. I build products, write about ideas worth savouring, and make art.

72 |

Every two weeks, I send out a newsletter where I share the stuff I create, as well as learnings and other cool finds. 73 |

Enter your email below to stay in touch.

74 | 75 |
76 | 77 |
78 | 79 | 80 | 81 |

This website is a growing collection of ideas I can't stop thinking about. And I experiment with various types of media to express them.

82 |

This includes topics like knowledge management, entrepreneurship, productivity, language learning, food, and dreaming about the future. I also share resources for learning product management on Informed PM. If you would like to check out my writings, here is a full list of my essays.

83 | 84 |

Join me in examining the weirdness, teasing apart the complexity, and more fully appreciating the beauty of the human condition.

85 | 86 |
87 | 88 |
89 | 90 | 91 |
92 | 93 | 98 |
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | {% if not debug %} 107 | 108 | 109 | 116 | {% endif %} 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /templates/frontend/new_short_link.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 | 6 |
7 | 8 | {% load static %} 9 | 10 | {% endblock content %} 11 | -------------------------------------------------------------------------------- /templates/frontend/space_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 |
6 | 7 |
8 | 9 | {% load static %} 10 | 11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /templates/frontend/vaas_header.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "lib": [ 6 | "ES6", 7 | "dom" 8 | ], 9 | "module": "es6", 10 | "target": "es6", 11 | "jsx": "react", 12 | "allowJs": true, 13 | "removeComments": true, 14 | "strictNullChecks": false, 15 | "moduleResolution": "node" 16 | } 17 | } -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | # path('', views.index ), 7 | path('search', views.search), 8 | path('sites/john', views.knowledge_artist, name='knowledge_artist'), 9 | path('sites/validate', views.validate_aas, name='validate_aas'), 10 | path('sites/coding_utility', views.coding_utility, name='coding_utility'), 11 | path('code/', views.coding_lookup_article, name='coding_lookup_article'), 12 | path('about_validate', views.about_validate, name='about_validate'), 13 | 14 | 15 | path('spaces/', views.space_detail, name='space_detail'), 16 | path('spaces//presentation', views.space_presentation), 17 | path('resources/', views.resource_detail), 18 | path('activities/', views.activity_detail), 19 | 20 | # the old Rumin blog 21 | path('blog', views.rumin_blog, name='blog'), 22 | path('blog/', views.blog_detail), 23 | 24 | # Knowldge Artist 25 | path('newsletter', views.ka_newsletter), 26 | path('newsletter/', views.ka_newsletter_post), 27 | path('music', views.ka_music), 28 | path('drawings', views.ka_drawings), 29 | path('articles', views.ka_articles), 30 | path('articles/future-of-search', views.ka_future_of_search), 31 | path('article/', views.ka_article_short_url), 32 | path('articles/', views.ka_article_detail), 33 | path('articles//', views.ka_article_detail), 34 | 35 | # the old r/BJJ 36 | path('wiki/', views.wiki_detail), 37 | path('url/', views.saved_url), 38 | path('s/new', views.new_short_link), 39 | path('r/grappling', views.bjj_page), 40 | path('r/bjj', views.bjj_page), 41 | path('explorer', views.explorer), 42 | path('expdemo', views.explorer), 43 | path('demo_graph', views.demo_graph), 44 | path('demo_log', views.demo_log), 45 | path('demo_writer', views.demo_writer), 46 | path('demo_grid_board', views.demo_grid_board), 47 | path('demo_mood_board', views.demo_mood_board), 48 | path('demo_log_list', views.demo_log_list), 49 | ] 50 | -------------------------------------------------------------------------------- /views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.urls import reverse 3 | from spaces.models import Space, serialize_html 4 | 5 | def index(request): 6 | return render(request, 'frontend/index.html') 7 | 8 | def user_profile(request, pk): 9 | return render(request, 'frontend/index.html') 10 | 11 | def knowledge_artist(request): 12 | return render(request, 'frontend/knowledge_artist.html') 13 | 14 | def validate_aas(request): 15 | return render(request, 'frontend/validate_aas.html') 16 | 17 | def coding_utility(request): 18 | return render(request, 'frontend/coding_utility.html') 19 | 20 | def coding_lookup_article(request, short_url): 21 | space = Space.objects.filter(short_url=short_url).first() 22 | if not space or not space.is_public: 23 | return render(request, 'frontend/ka_404.html') 24 | 25 | return render(request, 'frontend/coding_lookup_article.html', { 'space': space, 'html_body': serialize_html({ 'children': space.json_body }), 'keywords': space.custom_fields.get('meta_keywords'), 'description': space.custom_fields.get('meta_description') }) 26 | 27 | def about_validate(request): 28 | return render(request, 'frontend/about_validate.html') 29 | 30 | def search(request): 31 | return render(request, 'frontend/index.html') 32 | 33 | def space_detail(request, pk): 34 | return render(request, 'frontend/index.html') 35 | 36 | def space_presentation(request, pk): 37 | return render(request, 'frontend/index.html') 38 | 39 | def resource_detail(request, pk): 40 | return render(request, 'frontend/index.html') 41 | 42 | def rumin_blog(request): 43 | return render(request, 'frontend/index.html') 44 | 45 | def explorer(request): 46 | return render(request, 'frontend/explorer.html') 47 | 48 | def blog_detail(request, pk): 49 | # Dropshipping, part 1 50 | if pk == '2dd7efe6-b375-4a39-99ad-36a156ba423c': 51 | return redirect('https://knowledgeartist.org/articles/fbd09646-e750-48fd-a90f-7f8fb9b6f456/everything-i-learned-starting-a-6000mo-dropshipping-business') 52 | 53 | # What is Zettelkasten 54 | if pk == '64b01dcd-1890-4817-9887-42fb9575614c': 55 | return redirect('https://knowledgeartist.org/articles/64b01dcd-1890-4817-9887-42fb9575614c/what-is-zettelkasten') 56 | 57 | # masks for all, traditional Chinese 58 | if pk == '0a7db3f3-f5a4-4e44-956d-0c8d73e55050': 59 | return redirect('https://knowledgeartist.org/articles/0a7db3f3-f5a4-4e44-956d-0c8d73e55050') 60 | 61 | # masks for all, simplified 62 | if pk == 'd7e7c832-51bc-4e7d-9684-d822ed91ac96': 63 | return redirect('https://knowledgeartist.org/articles/d7e7c832-51bc-4e7d-9684-d822ed91ac96') 64 | 65 | return render(request, 'frontend/index.html') 66 | 67 | def ka_newsletter(request): 68 | return render(request, 'knowledge_artist/newsletter.html') 69 | 70 | def ka_music(request): 71 | return render(request, 'knowledge_artist/music.html') 72 | 73 | def ka_drawings(request): 74 | return render(request, 'knowledge_artist/drawings.html') 75 | 76 | def ka_newsletter_post(request, short_url): 77 | space = Space.objects.filter(short_url=short_url).first() 78 | if not space or not space.is_public: 79 | return render(request, 'frontend/ka_404.html') 80 | 81 | return render(request, 'frontend/ka_article.html', { 82 | 'space': space, 83 | 'html_title': space.html_title, 84 | 'html_body': serialize_html({ 85 | 'children': space.json_body }), 86 | 'book_rating': space.custom_fields.get('book_rating'), 87 | 'book_amazon_url': space.custom_fields.get('book_amazon_url'), 88 | 'keywords': space.custom_fields.get('meta_keywords'), 89 | 'description': space.custom_fields.get('meta_description'), 90 | 'show_newsletter_prompt': (not space.custom_fields.get('disable_newsletter_prompt')), 91 | 'is_newsletter': True 92 | }) 93 | 94 | def ka_articles(request): 95 | return render(request, 'frontend/ka_articles.html') 96 | 97 | def ka_article_detail(request, pk, slug=None): 98 | space = Space.objects.get(pk=pk) 99 | if not space.is_public: 100 | return render(request, 'frontend/ka_404.html') 101 | 102 | if space.short_url: 103 | return redirect('/article/' + space.short_url) 104 | 105 | return render(request, 'frontend/ka_article.html', { 'space': space, 'html_body': serialize_html({ 'children': space.json_body }), 'keywords': space.custom_fields.get('meta_keywords'), 'description': space.custom_fields.get('meta_description'), 'show_newsletter_prompt': (not space.custom_fields.get('disable_newsletter_prompt')) }) 106 | 107 | def ka_article_short_url(request, short_url): 108 | space = Space.objects.filter(short_url=short_url).first() 109 | if not space or not space.is_public: 110 | return render(request, 'frontend/ka_404.html') 111 | return render(request, 'frontend/ka_article.html', { 112 | 'space': space, 113 | 'html_title': space.html_title, 114 | 'html_body': serialize_html({ 'children': space.json_body }), 115 | 'keywords': space.custom_fields.get('meta_keywords'), 116 | 'description': space.custom_fields.get('meta_description'), 117 | 'show_newsletter_prompt': (not space.custom_fields.get('disable_newsletter_prompt')) 118 | }) 119 | 120 | def ka_future_of_search(request): 121 | return redirect('/articles/243ee23e-0dfd-4b94-8b77-0216269b37f6/the-next-frontiers-of-search') 122 | # return render(request, 'frontend/ka_future_of_search.html') 123 | 124 | def wiki_detail(request, pk): 125 | return render(request, 'frontend/index.html') 126 | 127 | def bjj_page(request): 128 | return redirect('https://combatknowledge.com') 129 | # return render(request, 'combat_knowledge_site/home.html') 130 | # host = request.get_host() 131 | # if host == 'www.getrumin.com' or host == 'getrumin.com': 132 | # return redirect('https://combatknowledge.com/r/bjj') 133 | 134 | # return render(request, 'frontend/index.html') 135 | 136 | def new_short_link(request): 137 | return render(request, 'frontend/index.html') 138 | 139 | def activity_detail(request, pk): 140 | return redirect(reverse('space_detail', kwargs={'pk':pk})) 141 | # return render(request, 'frontend/activity_detail.html') 142 | 143 | def saved_url(request, encoded_url): 144 | return render(request, 'frontend/index.html') 145 | 146 | def demo_graph(request): 147 | return render(request, 'frontend/index.html') 148 | 149 | def demo_log(request): 150 | return render(request, 'frontend/index.html') 151 | 152 | def demo_collection_block(request): 153 | return render(request, 'frontend/demo_collection_block.html') 154 | 155 | def demo_writer(request): 156 | return render(request, 'frontend/index.html') 157 | 158 | def demo_grid_board(request): 159 | return render(request, 'frontend/index.html') 160 | 161 | def demo_mood_board(request): 162 | return render(request, 'frontend/index.html') 163 | 164 | def demo_log_list(request): 165 | return render(request, 'frontend/index.html') 166 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.tsx', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/, 11 | }, 12 | { 13 | test: /\.css$/, 14 | use: [ 15 | // 'style-loader', 16 | 'css-loader' 17 | ] 18 | }, 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | // use: { 23 | // loader: "babel-loader" 24 | // } 25 | loader: 'babel-loader', 26 | options: { 27 | presets: ['@babel/preset-env', 28 | '@babel/react',{ 29 | 'plugins': ['@babel/plugin-proposal-class-properties']}] 30 | } 31 | } 32 | ], 33 | }, 34 | resolve: { 35 | extensions: [ '.tsx', '.ts', '.js' ], 36 | }, 37 | output: { 38 | filename: 'main2.js', 39 | path: path.resolve(__dirname, 'static/frontend'), 40 | }, 41 | watch: true, 42 | watchOptions: { 43 | poll: true, 44 | ignored: /node_modules/ 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // this file is no longer used 2 | // var path = require('path'); 3 | 4 | // module.exports = { 5 | // entry: './src/index.tsx', 6 | // module: { 7 | // rules: [ 8 | // { 9 | // test: /\.tsx?$/, 10 | // use: 'ts-loader', 11 | // exclude: /node_modules/, 12 | // }, 13 | // { 14 | // test: /\.css$/, 15 | // use: [ 16 | // // 'style-loader', 17 | // 'css-loader' 18 | // ] 19 | // }, 20 | // { 21 | // test: /\.js$/, 22 | // exclude: /node_modules/, 23 | // // use: { 24 | // // loader: "babel-loader" 25 | // // } 26 | // loader: 'babel-loader', 27 | // options: { 28 | // presets: ['@babel/preset-env', 29 | // '@babel/react',{ 30 | // 'plugins': ['@babel/plugin-proposal-class-properties']}] 31 | // } 32 | // } 33 | // ], 34 | // }, 35 | // resolve: { 36 | // extensions: [ '.tsx', '.ts', '.js' ], 37 | // }, 38 | // output: { 39 | // filename: 'main2.js', 40 | // path: path.resolve(__dirname, 'static/frontend'), 41 | // }, 42 | // watch: true, 43 | // watchOptions: { 44 | // poll: true, 45 | // ignored: /node_modules/ 46 | // } 47 | // }; 48 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js'); 2 | const merge = require('webpack-merge'); 3 | const path = require('path'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | output: { 9 | filename: 'main.js', 10 | path: path.resolve(__dirname, 'static/frontend'), 11 | } 12 | }); -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const path = require('path'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'production', 8 | output: { 9 | filename: 'main.js', 10 | path: path.resolve(__dirname, 'static/frontend'), 11 | }, 12 | optimization: { 13 | minimize: true, 14 | minimizer: [new TerserPlugin()], 15 | } 16 | }); --------------------------------------------------------------------------------