├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── screenshot.png └── src ├── App ├── ControlBar │ ├── assets │ │ ├── icon-close-black.svg │ │ ├── icon-close-white.svg │ │ ├── icon-day.svg │ │ ├── icon-night.svg │ │ ├── icon-share-black.svg │ │ └── icon-share-white.svg │ ├── icons │ │ └── logo.js │ ├── index.css │ └── index.js ├── Greeting │ ├── index.css │ └── index.js ├── Slide │ ├── assets │ │ ├── icon-download.svg │ │ └── icon-link.svg │ ├── index.css │ └── index.js ├── Slideshow │ ├── index.css │ └── index.js ├── index.css └── index.js ├── index.css ├── index.js └── registerServiceWorker.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "env" ] 3 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["eslint:recommended", "react-app"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "indent": [ "error", 4, { "SwitchCase": 1 } ], 15 | "linebreak-style": [ "error", "unix" ], 16 | "quotes": [ "error", "single" ], 17 | "semi": [ "error", "never" ], 18 | "no-console": [ "off" ], 19 | "import/no-webpack-loader-syntax": ["off"] 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](screenshot.png) 2 | 🔗[Show Are.na](https://bryantwells.github.io/show-arena) 3 | 4 | **What Is It** 5 | Show Are.na is built on top of the [Are.na API](http://dev.are.na), formatting a given channel's content into a lightweight slideshow. 6 | 7 | **Why Is It** 8 | As research accumulates within a channel on [Are.na](http://www.are.na) — this tool can be used to help present/articulate a central idea. 9 | 10 | **To Do** 11 | * [x] Navigate with keyboard (l/r) 12 | * [x] Toggle light/dark mode 13 | * [x] Toggle show/hide UI 14 | * [x] Configure UI settings via query parameters 15 | * [x] Get shareable link via UI 16 | * [x] Support all block types 17 | * [ ] Toggle block description visibility 18 | * [ ] Toggle block title visibility 19 | * [ ] Lazy loading 20 | * [ ] Save for offline use 21 | * [ ] Authentication for private channels 22 | * [ ] Channel search within homepage 23 | 24 | **Known Bugs** 25 | * Shared links do not work in Safari (404) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "show-arena", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "query-string": "^5.1.1", 7 | "react": "^16.4.0", 8 | "react-copy-to-clipboard": "^5.0.1", 9 | "react-dom": "^16.4.0", 10 | "react-router-dom": "^4.2.2", 11 | "react-scripts": "1.1.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject", 18 | "predeploy": "npm run build", 19 | "deploy": "gh-pages -d build" 20 | }, 21 | "homepage": "https://bryantwells.github.io/show-arena/", 22 | "devDependencies": { 23 | "gh-pages": "^1.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryantwells/show-arena/149758c80c3c9e7cc70ba7b1cf6615b988107d5c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Show Are.na 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryantwells/show-arena/149758c80c3c9e7cc70ba7b1cf6615b988107d5c/screenshot.png -------------------------------------------------------------------------------- /src/App/ControlBar/assets/icon-close-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-close-black 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App/ControlBar/assets/icon-close-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-close-white 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App/ControlBar/assets/icon-day.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-day 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/App/ControlBar/assets/icon-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-night 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App/ControlBar/assets/icon-share-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-share-black 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App/ControlBar/assets/icon-share-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-share-white 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App/ControlBar/icons/logo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Logo extends Component { 4 | 5 | render () { 6 | 7 | return ( 8 | 9 | 10 | 11 | ) 12 | 13 | } 14 | 15 | } 16 | 17 | export default Logo 18 | -------------------------------------------------------------------------------- /src/App/ControlBar/index.css: -------------------------------------------------------------------------------- 1 | /* CONTROL BAR */ 2 | 3 | .ControlBar { 4 | z-index: 5; 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | display: flex; 10 | justify-content: flex-end; 11 | padding: 10px; 12 | transition: opacity .25s; 13 | } 14 | 15 | [data-night-mode="false"] .ControlBar { 16 | color: black; 17 | } 18 | 19 | [data-ui="false"] .ControlBar { 20 | opacity: 0; 21 | } 22 | 23 | [data-ui="false"] .ControlBar:hover { 24 | opacity: 1; 25 | } 26 | 27 | 28 | /* TITLE */ 29 | 30 | .ControlBar-channelTitle { 31 | display: flex; 32 | font-size: var(--sm-font-size); 33 | justify-content: center; 34 | align-items: center; 35 | } 36 | 37 | .ControlBar-logo { 38 | height: 1em; 39 | width: auto; 40 | margin-right: .25em; 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | [data-night-mode="true"] .ControlBar-logo { 46 | fill: white; 47 | } 48 | 49 | [data-night-mode="false"] .ControlBar-logo{ 50 | fill: black; 51 | } 52 | 53 | .ControlBar-logo svg { 54 | height: 100%; 55 | width: auto; 56 | } 57 | 58 | 59 | /* GROUP */ 60 | 61 | .ControlBar-group { 62 | flex: 1; 63 | display: flex; 64 | } 65 | 66 | .ControlBar-group:last-child { 67 | justify-content: flex-end; 68 | } 69 | 70 | .ControlBar-group--channelTitle { 71 | justify-content: center; 72 | align-items: center; 73 | } 74 | 75 | 76 | /* CONTROL */ 77 | 78 | .Control { 79 | cursor: pointer; 80 | padding: 10px 10px; 81 | display: flex; 82 | align-items: center; 83 | opacity: 0.5; 84 | transition: opacity 0.15s; 85 | } 86 | 87 | .Control:hover { 88 | opacity: 1; 89 | } 90 | 91 | .Control--share { 92 | position: relative; 93 | } 94 | 95 | 96 | /* TOGGLE */ 97 | 98 | .Control-toggle { 99 | border: 2px solid black; 100 | display: flex; 101 | padding: 3px 5px; 102 | border-radius: 10px; 103 | width: 60px; 104 | position: relative; 105 | font-size: 10px; 106 | } 107 | 108 | .Control-toggle.is-off { 109 | justify-content: flex-end; 110 | } 111 | 112 | [data-night-mode="true"] .Control-toggle { 113 | border-color: white; 114 | } 115 | 116 | 117 | /* TOGGLE SLIDER */ 118 | 119 | .Control-toggleSlider { 120 | width: 13px; 121 | height: 13px; 122 | background-color: black; 123 | position: absolute; 124 | right: 2px; 125 | top: 2px; 126 | border-radius: 50%; 127 | transition: right .15s; 128 | } 129 | 130 | .Control-toggle.is-off .Control-toggleSlider { 131 | right: calc(100% - 15px); 132 | top: 2px; 133 | } 134 | 135 | [data-night-mode="true"] .Control-toggleSlider { 136 | background-color: white; 137 | } 138 | 139 | 140 | /* CONTROL LABEL */ 141 | 142 | .Control-label { 143 | font-size: 10px; 144 | font-weight: bold; 145 | } 146 | 147 | .Control-label--on { 148 | margin-left: 3px; 149 | } 150 | 151 | .Control-toggle.is-off .Control-label--on { 152 | display: none; 153 | } 154 | 155 | .Control-toggle.is-on .Control-label--off { 156 | display: none; 157 | } 158 | 159 | .Control-label--copied { 160 | opacity: 0; 161 | transform: translateY(25%); 162 | transition: 0.25s opacity, 0.25s transform; 163 | position: absolute; 164 | right: 100%; 165 | pointer-events: none; 166 | } 167 | 168 | .Control-label--copied.is-active { 169 | opacity: 1; 170 | transform: translateY(0%); 171 | } 172 | 173 | [data-night-mode="true"] .ControlBar-label--copied { 174 | color: white; 175 | background-color: black; 176 | } 177 | 178 | [data-night-mode="false"] .ControlBar-label--copied { 179 | color: black; 180 | background-color: white; 181 | } 182 | 183 | 184 | /* CONTROL ICON */ 185 | 186 | .Control-icon { 187 | height: 30px; 188 | width: 30px; 189 | display: block; 190 | background-size: contain; 191 | } 192 | 193 | .Control-icon--night { 194 | background-image: url('./assets/icon-night.svg'); 195 | } 196 | 197 | [data-night-mode="true"] .Control-icon--night { 198 | display: none; 199 | } 200 | 201 | .Control-icon--day { 202 | background-image: url('./assets/icon-day.svg'); 203 | } 204 | 205 | [data-night-mode="false"] .Control-icon--day { 206 | display: none; 207 | } 208 | 209 | [data-night-mode="true"] .Control-icon--close { 210 | background-image: url('./assets/icon-close-white.svg'); 211 | } 212 | 213 | [data-night-mode="false"] .Control-icon--close { 214 | background-image: url('./assets/icon-close-black.svg'); 215 | } 216 | 217 | [data-night-mode="true"] .Control-icon--share { 218 | background-image: url('./assets/icon-share-white.svg'); 219 | } 220 | 221 | [data-night-mode="false"] .Control-icon--share { 222 | background-image: url('./assets/icon-share-black.svg'); 223 | } 224 | -------------------------------------------------------------------------------- /src/App/ControlBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { CopyToClipboard } from 'react-copy-to-clipboard' 3 | import Logo from './icons/logo' 4 | import './index.css' 5 | 6 | class ControlBar extends Component { 7 | 8 | constructor (props) { 9 | super(props) 10 | 11 | this.state = { 12 | shareUrlIsCopied: false 13 | } 14 | 15 | this.handleOnCopy = this.handleOnCopy.bind(this) 16 | } 17 | 18 | handleOnCopy () { 19 | this.setState({ shareUrlIsCopied: true }) 20 | setTimeout(() => { 21 | this.setState({ shareUrlIsCopied: false }) 22 | }, 750) 23 | } 24 | 25 | render () { 26 | 27 | return ( 28 |
29 |
30 | 31 |
{ e.stopPropagation(); this.props.toggleSetting('ui') }}> 33 |
34 | UI ON 35 | UI OFF 36 |
37 |
38 |
39 | 40 |
{ e.stopPropagation(); this.props.toggleSetting('nightMode') }}> 42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 | 56 | {this.props.title} 57 | 58 |
59 |
60 | 61 |
62 | 63 |
{ e.stopPropagation() }}> 64 |
65 |

COPIED

66 |
67 | 68 |
69 |
70 |
71 | 72 |
{ e.stopPropagation(); this.props.history.push('/') }}> 74 |
75 |
76 | 77 |
78 |
79 | ) 80 | 81 | } 82 | 83 | } 84 | 85 | export default ControlBar 86 | -------------------------------------------------------------------------------- /src/App/Greeting/index.css: -------------------------------------------------------------------------------- 1 | .Greeting { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | min-height: 100vh; 7 | font-weight: bold; 8 | } 9 | 10 | .Greeting-statement { 11 | font-size: var(--md-font-size); 12 | } 13 | 14 | .Greeting-statement a { 15 | color: rgba(255,255,255,0.65); 16 | } 17 | 18 | .Greeting-statement a:hover { 19 | color: rgba(255,255,255,1); 20 | } 21 | 22 | .Greeting-form { 23 | width: 100%; 24 | text-align: center; 25 | } 26 | 27 | .Greeting-textInput { 28 | display: block; 29 | margin: auto; 30 | width: 550px; 31 | max-width: 100%; 32 | font-size: var(--base-font-size); 33 | outline: none; 34 | padding: 10px; 35 | background-color: var(--light-grey); 36 | border: 1px solid var(--dark-grey); 37 | border-radius: .25em; 38 | font-family: monospace; 39 | text-align: center; 40 | font-size: var(--sm-font-size); 41 | } 42 | 43 | .Greeting-submit { 44 | margin-top: 2em; 45 | font-size: 1rem; 46 | background: transparent; 47 | color: white; 48 | padding: .5em 2em; 49 | border-radius: .25em; 50 | } 51 | 52 | .Greeting-submit:hover { 53 | background-color: rgba(255,255,255,0.15); 54 | cursor: pointer; 55 | } 56 | -------------------------------------------------------------------------------- /src/App/Greeting/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './index.css' 3 | 4 | class Greeting extends Component { 5 | 6 | constructor () { 7 | super() 8 | this.state = { value: '' } 9 | 10 | this.handleChange = this.handleChange.bind(this) 11 | this.handleSubmit = this.handleSubmit.bind(this) 12 | } 13 | 14 | handleChange (e) { 15 | this.setState({ value: e.target.value }) 16 | } 17 | 18 | handleSubmit (e) { 19 | e.preventDefault() 20 | 21 | // (if the target url is valid) get the slug 22 | // navigate to the slideshow page 23 | const value = this.state.value 24 | if (value && value.lastIndexOf('/') >= 0) { 25 | const slug = value.substr(value.lastIndexOf('/') + 1) 26 | this.props.history.push(`/${ slug }/0`) 27 | } 28 | } 29 | 30 | render () { 31 | 32 | return ( 33 |
34 |

35 | Enter an Are.na channel URL: 36 |

37 |
39 | 43 | 45 |
46 |
47 | ) 48 | 49 | } 50 | 51 | } 52 | 53 | export default Greeting 54 | -------------------------------------------------------------------------------- /src/App/Slide/assets/icon-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App/Slide/assets/icon-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App/Slide/index.css: -------------------------------------------------------------------------------- 1 | /* SLIDE */ 2 | 3 | .Slide { 4 | height: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | padding: 70px; 9 | position: relative; 10 | user-select: none; 11 | } 12 | 13 | [data-night-mode="false"] .Slide { 14 | color: black; 15 | } 16 | 17 | .Slide:not(.is-active) { 18 | display: none; 19 | } 20 | 21 | .Slide img { 22 | max-height: 100%; 23 | object-fit: contain; 24 | background-color: rgba(125,125,125,0.05); 25 | } 26 | 27 | .Slide iframe { 28 | max-height: 100%; 29 | width: 800px; 30 | height: 600px; 31 | } 32 | 33 | .Slide-title { 34 | max-width: 100%; 35 | font-size: var(--sm-font-size); 36 | font-weight: normal; 37 | } 38 | 39 | .Slide-spacer { 40 | flex: 1; 41 | } 42 | 43 | .Slide-link { 44 | display: block; 45 | } 46 | 47 | .Slide-icon { 48 | display: block; 49 | position: absolute; 50 | width: 30px; 51 | height: 30px; 52 | border-radius: 5px; 53 | border-left: 2px solid var(--dark-grey); 54 | border-bottom: 2px solid var(--dark-grey); 55 | } 56 | 57 | .Slide-icon--download { 58 | bottom: 50px; 59 | right: 50px; 60 | } 61 | 62 | .Slide-icon--link { 63 | bottom: 10px; 64 | right: 10px; 65 | } 66 | 67 | 68 | /* CHANNEL SLIDE */ 69 | 70 | .Slide-link--channel { 71 | width: 425px; 72 | height: 425px; 73 | position: relative; 74 | text-align: center; 75 | display: flex; 76 | flex-direction: column; 77 | justify-content: center; 78 | padding: 20px; 79 | border: 3px solid; 80 | border-color: rgba(23,172,16,0.5); 81 | color: rgb(23,172,16); 82 | } 83 | 84 | .Slide-link--channel:hover { 85 | border-color: rgba(23,172,16,1); 86 | } 87 | 88 | .Slide-link--channel.is-closed { 89 | border-color: rgba(72,62,100,0.5); 90 | color: rgb(72,62,100); 91 | } 92 | 93 | .Slide-link--channel.is-closed:hover { 94 | border-color: rgba(72,62,100,1); 95 | } 96 | 97 | .Slide-channelTitle { 98 | flex: 0; 99 | font-weight: normal; 100 | } 101 | 102 | .Slide-channelStatList { 103 | font-size: var(--sm-font-size); 104 | flex: 1; 105 | padding: 0; 106 | list-style: none; 107 | } 108 | 109 | 110 | /* TEXT SLIDE */ 111 | 112 | .Slide-textBlock { 113 | font-family: Times, serif; 114 | max-width: 800px; 115 | line-height: 1.5; 116 | max-height: 100%; 117 | user-select: text; 118 | cursor: text; 119 | font-size: var(--md-font-size); 120 | } 121 | 122 | 123 | /* NON-SPECIFIC MEDIA SLIDE */ 124 | 125 | .Slide-mediaStamp { 126 | background-color: var(--light-grey); 127 | color: var(--dark-grey); 128 | width: 425px; 129 | height: 425px; 130 | display: flex; 131 | flex-direction: column; 132 | text-align: center; 133 | padding: 20px; 134 | border-radius: 20px; 135 | max-width: 100%; 136 | position: relative; 137 | z-index: 2; 138 | border-left: 5px solid var(--dark-grey); 139 | border-bottom: 5px solid var(--dark-grey); 140 | } 141 | 142 | .Slide-mediaExtension { 143 | text-transform: uppercase; 144 | font-size: var(--xl-font-size); 145 | font-weight: normal; 146 | } 147 | 148 | .Slide-mediaTitle { 149 | flex: 1; 150 | font-size: var(--sm-font-size); 151 | font-weight: normal; 152 | margin: 0; 153 | display: flex; 154 | justify-content: center; 155 | align-items: flex-end; 156 | } 157 | 158 | 159 | /* WEB SLIDE */ 160 | 161 | .Slide-link--web { 162 | max-width: 500px; 163 | text-align: center; 164 | position: relative; 165 | } 166 | 167 | .Slide-link--web .Slide-title { 168 | opacity: 0.5; 169 | position: absolute; 170 | top: 100%; 171 | left: 0; 172 | right: 0; 173 | } 174 | 175 | .Slide-link--web:hover .Slide-title { 176 | opacity: 1; 177 | } 178 | -------------------------------------------------------------------------------- /src/App/Slide/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './index.css' 3 | import downloadIcon from './assets/icon-download.svg' 4 | import linkIcon from './assets/icon-link.svg' 5 | 6 | class Slide extends Component { 7 | 8 | render () { 9 | 10 | const slideClass = this.props.slideInfo.class 11 | const className = (this.props.isActive) 12 | ? 'Slide is-active' 13 | : 'Slide' 14 | 15 | switch (slideClass) { 16 | 17 | case 'Image': { 18 | 19 | const src = this.props.slideInfo.image.original.url 20 | const title = this.props.slideInfo.title 21 | 22 | return ( 23 |
  • 24 | {title} 25 |
  • 26 | ) 27 | 28 | } 29 | 30 | case 'Media': { 31 | 32 | const html = this.props.slideInfo.embed.html 33 | 34 | return ( 35 |
  • 37 |
  • 38 | ) 39 | 40 | } 41 | 42 | case 'Attachment': { 43 | 44 | const extension = this.props.slideInfo.attachment.extension 45 | const attachmentSrc = this.props.slideInfo.attachment.url 46 | 47 | switch (extension) { 48 | 49 | case ('pdf'): { 50 | 51 | const thumbmailSrc = this.props.slideInfo.image.display.url 52 | const title = this.props.slideInfo.title 53 | 54 | return ( 55 |
  • 56 | {title} 57 | e.stopPropagation()}> 60 | download 61 | 62 |
  • 63 | ) 64 | 65 | } 66 | 67 | default: { 68 | 69 | const extension = this.props.slideInfo.attachment.extension 70 | const title = this.props.slideInfo.title 71 | 72 | return ( 73 |
  • 74 |
    75 |
    76 |

    .{extension}

    77 |

    {title}

    78 |
    79 | e.stopPropagation()}> 82 | download 83 | 84 |
  • 85 | ) 86 | 87 | } 88 | 89 | } 90 | 91 | } 92 | 93 | case 'Channel': { 94 | 95 | const title = this.props.slideInfo.title 96 | const author = this.props.slideInfo.user.full_name 97 | const size = this.props.slideInfo.length 98 | const url = `https://are.na/${ this.props.slideInfo.user.slug }/${ this.props.slideInfo.slug }` 99 | const channelClassName = (!this.props.slideInfo.open) 100 | ? 'Slide-link Slide-link--channel is-closed' 101 | : 'Slide-link Slide-link--channel' 102 | 103 | return ( 104 |
  • 105 | e.stopPropagation()}> 108 |
    109 |

    {title}

    110 | 114 |
    115 |
  • 116 | ) 117 | 118 | } 119 | 120 | case 'Text': { 121 | 122 | const html = this.props.slideInfo.content_html 123 | 124 | return ( 125 |
  • 126 |
    e.stopPropagation()}> 129 |
    130 |
  • 131 | ) 132 | 133 | } 134 | 135 | case 'Link': { 136 | 137 | const title = this.props.slideInfo.title 138 | const url = this.props.slideInfo.source.url 139 | const src = this.props.slideInfo.image.display.url 140 | 141 | return ( 142 |
  • 143 | e.stopPropagation()}> 146 | {title} 147 | link 149 |

    {title}

    150 |
    151 |
  • 152 | ) 153 | 154 | } 155 | 156 | default: { 157 | 158 | return ( 159 |
  • 160 |

    Content type not supported.

    161 |
  • 162 | ) 163 | 164 | } 165 | 166 | } 167 | 168 | } 169 | 170 | } 171 | 172 | export default Slide 173 | -------------------------------------------------------------------------------- /src/App/Slideshow/index.css: -------------------------------------------------------------------------------- 1 | /* SLIDESHOW */ 2 | 3 | .Slideshow:focus { 4 | outline:none; 5 | } 6 | 7 | .Slideshow[data-night-mode="true"] { 8 | background-color: black; 9 | } 10 | 11 | .Slideshow[data-night-mode="false"] { 12 | background-color: white; 13 | } 14 | 15 | 16 | /* SLIDELIST */ 17 | 18 | .Slideshow-slideList { 19 | height: 100vh; 20 | position: relative; 21 | margin: 0; 22 | padding: 0; 23 | list-style: none; 24 | cursor: e-resize; 25 | } 26 | 27 | 28 | /* BACK BUTTON */ 29 | 30 | .Slideshow-back { 31 | position: absolute; 32 | top: 0; 33 | right: 70vw; 34 | bottom: 0; 35 | left: 0; 36 | z-index: 2; 37 | cursor: w-resize; 38 | } 39 | 40 | /* MESSAGE */ 41 | 42 | .Message { 43 | height: 100vh; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | -------------------------------------------------------------------------------- /src/App/Slideshow/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import queryString from 'query-string' 4 | import Slide from '../Slide' 5 | import ControlBar from '../ControlBar' 6 | import './index.css' 7 | 8 | class Slideshow extends Component { 9 | 10 | constructor (props) { 11 | super(props) 12 | 13 | this.state = { 14 | channel: {}, 15 | shareUrl: 'http://', 16 | activeSlide: 0, 17 | error: null, 18 | settings: { 19 | nightMode: true, 20 | ui: true 21 | } 22 | } 23 | 24 | this.incrementSlide = this.incrementSlide.bind(this) 25 | this.handleKeyDown = this.handleKeyDown.bind(this) 26 | this.toggleSetting = this.toggleSetting.bind(this) 27 | this.updateShareUrl = this.updateShareUrl.bind(this) 28 | 29 | } 30 | 31 | componentDidMount () { 32 | const slug = this.props.match.params.slug 33 | const activeSlide = Number(this.props.match.params.activeSlide) 34 | ReactDOM.findDOMNode(this).focus() 35 | 36 | // Apply settings from URL query string 37 | this.registerQueryString() 38 | 39 | // Fetch data from API via slug 40 | fetch(`https://api.are.na/v2/channels/${ slug }`) 41 | .then((response) => { 42 | 43 | if (response.ok) { 44 | return response.json() 45 | } else { 46 | throw new Error(response.status) 47 | } 48 | 49 | }) 50 | .then((data) => { 51 | 52 | this.setState({ 53 | channel: { 54 | ...data, 55 | contents: data.contents.reverse() 56 | }, 57 | activeSlide: (activeSlide < data.contents.length) 58 | ? activeSlide 59 | : 0 60 | }) 61 | 62 | console.log(this.state.channel) 63 | 64 | }) 65 | .catch(error => this.setState({ error: error })) 66 | 67 | } 68 | 69 | componentDidUpdate (prevProps, prevState) { 70 | // Focus the page (to register keyboard events) 71 | ReactDOM.findDOMNode(this).focus() 72 | 73 | // update share URL 74 | if (prevState.settings !== this.state.settings) { 75 | this.updateShareUrl() 76 | } 77 | } 78 | 79 | componentWillReceiveProps (newProps) { 80 | // Apply active slide props 81 | const activeSlide = newProps.match.params.activeSlide 82 | 83 | this.setState({ 84 | activeSlide: Number(activeSlide) 85 | }) 86 | 87 | } 88 | 89 | handleKeyDown (e) { 90 | // Change slide with left & right arrow keys 91 | if (e.keyCode == '37') { // eslint-disable-line eqeqeq 92 | this.decrementSlide() 93 | } else if (e.keyCode == '39') { // eslint-disable-line eqeqeq 94 | this.incrementSlide() 95 | } 96 | } 97 | 98 | incrementSlide () { 99 | const slug = this.props.match.params.slug 100 | const channelLength = this.state.channel.contents.length 101 | const targetSlide = this.state.activeSlide + 1 102 | 103 | if (targetSlide < channelLength) { 104 | this.props.history.push(`/${ slug }/${ targetSlide }${this.props.location.search}`) 105 | } 106 | 107 | } 108 | 109 | decrementSlide () { 110 | const slug = this.props.match.params.slug 111 | const targetSlide = this.state.activeSlide - 1 112 | 113 | if (targetSlide >= 0) { 114 | this.props.history.push(`/${ slug }/${ targetSlide }${this.props.location.search}`) 115 | } 116 | 117 | } 118 | 119 | toggleSetting (setting) { 120 | // helper function to easily toggle a (single) setting 121 | this.updateSetting(setting, !this.state.settings[setting]) 122 | } 123 | 124 | updateSetting (key, value) { 125 | // Update the settings state from key & value strings or(!) arrays 126 | const settings = {...this.state.settings} 127 | 128 | if (typeof key === 'string') { 129 | key = [key] 130 | value = [value] 131 | } 132 | 133 | key.forEach((key, i) => { settings[key] = value[i] }) 134 | this.setState({ settings }) 135 | } 136 | 137 | registerQueryString () { 138 | // Build an array of keys and values, then apply the settings 139 | const queryObject = queryString.parse(this.props.location.search) 140 | let settingKeys = [] 141 | let settingValues = [] 142 | 143 | for (const queryKey in queryObject) { 144 | const settings = this.state.settings 145 | const matchedKey = Object.keys(settings).find((settingKey) => { 146 | return settingKey === queryKey 147 | }) 148 | 149 | if (matchedKey) { 150 | const settingValue = this.state.settings[matchedKey] 151 | const queryValue = (queryObject[queryKey] === 'true') 152 | ? true 153 | : (queryObject[queryKey] === 'false') 154 | ? false 155 | : queryObject[queryKey] 156 | 157 | if (typeof settingValue === typeof queryValue) { 158 | settingKeys.push(queryKey) 159 | settingValues.push(queryValue) 160 | } 161 | 162 | } 163 | 164 | } 165 | 166 | this.updateSetting(settingKeys, settingValues) 167 | } 168 | 169 | updateShareUrl () { 170 | const url = window.location.href 171 | const slug = this.props.match.params.slug 172 | const origin = url.slice(0, url.indexOf(slug) - 1) 173 | const query = queryString.stringify(this.state.settings) 174 | this.setState({ shareUrl: `${origin}/${slug}/?${query}` }) 175 | } 176 | 177 | render () { 178 | 179 | if (this.state.channel.id) { 180 | 181 | const slides = this.state.channel.contents 182 | const dataAttributes = {} 183 | 184 | // Convert settings to 'data-foo-bar' formatted attributes 185 | Object.entries(this.state.settings).forEach((entry) => { 186 | const key = entry[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() 187 | dataAttributes[`data-${key}`] = entry[1] 188 | }) 189 | 190 | const slideItems = slides.map((slide, i) => { 191 | 192 | const isActive = this.state.activeSlide === i 193 | 194 | return ( 195 | 198 | ) 199 | 200 | }) 201 | 202 | return ( 203 |
    207 |
    { e.stopPropagation(); this.decrementSlide(e) }}> 209 |
    210 | 217 |
      218 | {slideItems} 219 |
    220 |
    221 | ) 222 | 223 | } else if (this.state.error) { 224 | 225 | return ( 226 |
    227 |

    Could not complete request: {this.state.error}

    228 |
    229 | ) 230 | 231 | } else { 232 | 233 | return ( 234 |
    235 |

    get'ing the things...

    236 |
    237 | ) 238 | 239 | } 240 | 241 | } 242 | 243 | } 244 | 245 | export default Slideshow 246 | -------------------------------------------------------------------------------- /src/App/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryantwells/show-arena/149758c80c3c9e7cc70ba7b1cf6615b988107d5c/src/App/index.css -------------------------------------------------------------------------------- /src/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Router, Route } from 'react-router-dom' 3 | import { createBrowserHistory } from 'history' 4 | import Greeting from './Greeting' 5 | import Slideshow from './Slideshow' 6 | import './index.css' 7 | 8 | const history = createBrowserHistory({ basename: process.env.PUBLIC_URL }) 9 | 10 | class App extends Component { 11 | 12 | render () { 13 | 14 | return ( 15 | 16 |
    17 | 18 | 19 | 20 | 21 |
    22 |
    23 | ) 24 | 25 | } 26 | 27 | } 28 | 29 | export default App -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* RESET */ 2 | 3 | * { box-sizing: border-box } 4 | 5 | 6 | /* VARIBALES */ 7 | 8 | :root { 9 | --base-font-size: 18px; 10 | --sm-font-size: 0.75rem; 11 | --md-font-size: 1.5rem; 12 | --lg-font-size: 2.25rem; 13 | --xl-font-size: 4rem; 14 | --light-grey: #ebebec; 15 | --dark-grey: #9d9fa2; 16 | } 17 | 18 | 19 | /* BLOCK ELEMENTS */ 20 | 21 | html { 22 | background-color: black; 23 | font-size: var(--base-font-size); 24 | } 25 | 26 | body { 27 | margin: 0; 28 | padding: 0; 29 | font-family: Arial, sans-serif; 30 | color: white; 31 | } 32 | 33 | img { 34 | max-width: 100%; 35 | vertical-align: top; 36 | } 37 | 38 | 39 | /* TYPE */ 40 | 41 | h1, h2 { 42 | font-size: var(--lg-font-size); 43 | } 44 | 45 | a { 46 | color: unset; 47 | text-decoration: none; 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import registerServiceWorker from './registerServiceWorker' 4 | import App from './App/index.js' 5 | import './index.css' 6 | 7 | ReactDOM.render( , document.getElementById('Root')) 8 | 9 | registerServiceWorker() 10 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------