Skip to content
This repository was archived by the owner on Dec 28, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/KeyboardListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class KeyboardListener extends React.Component<Props> {
}

handleKeyDown(e: KeyboardEvent) {
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
// Don't do shortcuts while input has focus.
return
}
switch(e.code) {
case 'ArrowUp':
this.props.increaseFocusedMilestoneFn()
Expand Down
213 changes: 213 additions & 0 deletions components/SheetsControl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// @flow

import type { Milestone, MilestoneMap } from '../constants'
import { trackIds, tracks } from '../constants'

import React from 'react'

declare var gapi: any

const API_KEY = 'AIzaSyCPZccI1B543VHblD__af_JvV2b8Z5-Lis'
const CLIENT_ID = '124466069863-0uic3ahingc9bst2oc95h29nvu30lrnu.apps.googleusercontent.com'

Copy link

@rlopes rlopes Oct 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For that tool this is likely fine. But is that usually ok to have thing like secret keys and password pushed to Github?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels kinda weird, but I don't believe either of these things are actually a secret. I can't think of anything you could do with them as an attacker. See also my comment with the email screenshot.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I was asking because we had a blank policy of nothing like that in Github. I prefer we are more pragmatic and not just over cautious here instead.

const DISCOVERY_DOCS = ["https://sheets.googleapis.com/$discovery/rest?version=v4"]
const SCOPES = "https://www.googleapis.com/auth/spreadsheets"

const RANGE = `B1:b${trackIds.length}`

const DOCS_URL_REGEX = /^https:\/\/docs.google.com\/spreadsheets\/d\/([0-9a-zA-Z_\-]+)/

type Props = {
name: string,
onImport: (milestones: Milestone[]) => void,
milestoneByTrack: MilestoneMap
}

type State = {
isSignedIn: boolean,
sheetId: string
}

export default class SheetsControl extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isSignedIn: false,
sheetId: '',
}
}

componentDidMount() {
window.sheetsControl = this
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit odd to do that but until we have a better idea...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I know it's bad. I just wasn't sure of any better way.


componentDidUpdate(prevProps: Props, prevState: State) {
if (this.state.isSignedIn && !prevState.isSignedIn) {
this.importSheet()
}
}

componentWillUnmount() {
delete window.sheetsControl
}

initClient() {
console.log('initing')
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(() => {
console.log('promise resolved')
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(this.updateSigninStatus.bind(this))

// Handle the initial sign-in state.
this.updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get())
}).catch(error => {
console.log('init failed', error)
})
}

updateSigninStatus(isSignedIn: boolean) {
console.log('signed in', isSignedIn)
this.setState({ isSignedIn })
}

render() {
const style = <style jsx>{`
button,input {
font-size: 20px;
line-height: 20px;
margin-bottom: 20px;
margin-left: 3px;
min-width: 100px;
}
button {
border: 1;
background: #eee;
border-radius: 0px;
}
`}</style>

if (!this.state.isSignedIn) {
return (
<div>
{style}
<button onClick={this.handleAuthClick.bind(this)}>Authorize</button>
</div>
)
} else {
return (
<div>
{style}
<div>
<input
type="text"
value={this.state.sheetId}
onChange={this.handleSheetChange.bind(this)}
placeholder="Sheet ID"
/>
</div>
{this.state.sheetId &&
<div>
<a href={`https://docs.google.com/spreadsheets/d/${this.state.sheetId}/edit`} target="_blank">View Sheet</a>
</div>}
<button onClick={this.importSheet.bind(this)} disabled={!this.state.sheetId}>Import</button>
{this.state.sheetId
? <button onClick={this.handleSaveClick.bind(this)}>Save</button>
: <button onClick={this.handleCreateClick.bind(this)}>Create</button>}
<button onClick={this.handleSignOutClick.bind(this)}>Sign Out</button>
</div>
)
}
}

handleSheetChange(e: SyntheticEvent<HTMLButtonElement>) {
const val = e.currentTarget.value
const match = val.match(DOCS_URL_REGEX)
if (match) {
// URL pasted in
this.setState({ sheetId: match[1] })
} else {
this.setState({ sheetId: val })
}
}

handleAuthClick() {
gapi.auth2.getAuthInstance().signIn()
}

importSheet() {
if (!this.state.sheetId) {
return
}
console.log('importing sheet', this.state.sheetId)
// Get stuff from sheet
gapi.client.sheets.spreadsheets.values.get({
spreadsheetId: this.state.sheetId,
range: RANGE
}).then(response => {
console.log('imported sheet')
const range = response.result
if (range.values.length > 0) {
const milestones = range.values.map(val => parseInt(val[0]))
milestones.forEach(milestone => console.log(milestone))
this.props.onImport(milestones)
} else {
console.log('no values found')
}
})
}

handleSignOutClick() {
gapi.auth2.getAuthInstance().signOut()
}

handleCreateClick() {
const rows = trackIds.map(trackId => [tracks[trackId].displayName, this.props.milestoneByTrack[trackId]])
const data = rows.map((row, i) => ({
startRow: i,
rowData: {
values: [
{
userEnteredValue: {
stringValue: row[0]
}
},
{
userEnteredValue: {
numberValue: row[1]
}
}
]
}
}))
gapi.client.sheets.spreadsheets.create({
properties: {
title: `${this.props.name}'s Snowflake`
},
sheets: [ { data } ]
}).then(response => {
this.setState({ sheetId: response.result.spreadsheetId })
})
}

handleSaveClick() {
const values = trackIds.map(trackId => this.props.milestoneByTrack[trackId])
gapi.client.sheets.spreadsheets.values.update({
spreadsheetId: this.state.sheetId,
range: RANGE,
valueInputOption: 'USER_ENTERED',
resource: {
majorDimension: 'COLUMNS',
values: [ values ]
}
}).then(() => {
console.log('saved')
}).catch(() => {
console.log('error saving')
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally you could split this React component into several more granular components (each buttons) and compose them and move the Google API logic into a separate functions/file that you import here.
The dumber the component, the better 😄 !
Being said that can be done later in that kind of project.

13 changes: 13 additions & 0 deletions components/SnowflakeApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import KeyboardListener from '../components/KeyboardListener'
import LevelThermometer from '../components/LevelThermometer'
import NightingaleChart from '../components/NightingaleChart'
import PointSummaries from '../components/PointSummaries'
import SheetsControl from '../components/SheetsControl'
import React from 'react'
import TitleSelector from '../components/TitleSelector'
import Track from '../components/Track'
Expand Down Expand Up @@ -171,6 +172,10 @@ class SnowflakeApp extends React.Component<Props, SnowflakeAppState> {
onChange={e => this.setState({name: e.target.value})}
placeholder="Name"
/>
<SheetsControl
name={this.state.name}
onImport={this.handleSheetsImport.bind(this)}
milestoneByTrack={this.state.milestoneByTrack} />
<TitleSelector
milestoneByTrack={this.state.milestoneByTrack}
currentTitle={this.state.title}
Expand Down Expand Up @@ -214,6 +219,14 @@ class SnowflakeApp extends React.Component<Props, SnowflakeAppState> {
)
}

handleSheetsImport(milestones: Milestone[]) {
const milestoneByTrack = {}
milestones.forEach((milestone, i) => {
milestoneByTrack[trackIds[i]] = milestone
})
this.setState({ milestoneByTrack })
}

handleTrackMilestoneChange(trackId: TrackId, milestone: Milestone) {
const milestoneByTrack = this.state.milestoneByTrack
milestoneByTrack[trackId] = milestone
Expand Down
23 changes: 23 additions & 0 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import SnowflakeApp from '../components/SnowflakeApp'
import Head from 'next/head'

if (typeof window !== 'undefined') {
window.handleClientLoad = () => {
console.log('handleClientLoad')
const checkInit = () => {
if (window.sheetsControl) {
gapi.load('client', window.sheetsControl.initClient.bind(window.sheetsControl))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit hacky.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

} else {
setTimeout(checkInit, 500)
}
}
checkInit()
}
}

export default () => <div>
<Head>
<script
async
defer
src="https://apis.google.com/js/api.js"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On modern browsers async will take precedence over defer and defer will not apply.
I am wondering if you just want defer here to make sure it executes after the Google API and HTML is ready.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied this out of the Google example. 🤷‍♂️

Copy link

@rlopes rlopes Oct 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. They probably thought about giving an option that would work well in all browsers not just the most recent. Progressive degradation.

onLoad="this.onLoad = function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()" />
</Head>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export default () => (
  <div>
    <Head>
      <script defer src="https://apis.google.com/js/api.js" onLoad="handleClientLoad()" />
    </Head>
    <SnowflakeApp />
  </div>
);

This should be good enough because as soon as window.sheetsControl is available the checkInit call is not repeated.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, basically copied out of the Google example. 🤷‍♂️ https://developers.google.com/sheets/api/quickstart/js

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some older browser support again 🤔 ? Fair enough.
I can tell the simplified snippet worked for recent Chrome on my machine. Behaviour was as expected.

<SnowflakeApp />
</div>