├── .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 | [](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 |
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 |
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 |
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 |
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 |
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 |
15 | )
16 | }
17 |
18 | return(
19 |
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 |
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 |
115 |
124 |
125 |
134 |
135 |
145 |
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 |
2 |
3 |
4 |
5 |
13 |
14 |
22 |
23 |
24 |
25 |
26 |
27 |
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 |
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 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
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 |
11 |
12 |
13 |
14 |
22 |
23 |
43 |
44 |
45 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/templates/frontend/ka_more_articles.html:
--------------------------------------------------------------------------------
1 | {% include 'frontend/ka_articles_data.html' %}
2 |
3 |
8 |
9 |
26 |
27 |
54 |
--------------------------------------------------------------------------------
/templates/frontend/ka_share.html:
--------------------------------------------------------------------------------
1 |
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 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
18 | About
19 |
20 | FAQ
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 | });
--------------------------------------------------------------------------------