├── .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 | 
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 |
--------------------------------------------------------------------------------
/src/App/ControlBar/assets/icon-close-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/App/ControlBar/assets/icon-day.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/App/ControlBar/assets/icon-night.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/App/ControlBar/assets/icon-share-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/App/ControlBar/assets/icon-share-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
60 |
61 |
62 |
63 |
{ e.stopPropagation() }}>
64 |
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 |
46 |
47 | )
48 |
49 | }
50 |
51 | }
52 |
53 | export default Greeting
54 |
--------------------------------------------------------------------------------
/src/App/Slide/assets/icon-download.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/src/App/Slide/assets/icon-link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
57 | e.stopPropagation()}>
60 |
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 |
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 |
111 | - by {author}
112 | - {size} blocks
113 |
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 |
147 |
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 |
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 |
--------------------------------------------------------------------------------