<commit_before>import PropTypes from 'prop-types'
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import Navbar from './components/Navbar'
import { AppsActions } from './store/apps'
import { Box, Flex, Type } from 'blockstack-ui'
import { Hover } from 'react-powerplug'
import { Spinner } from '@components/ui/components/spinner'
import { trackEventOnce } from '@utils/server-utils'

const Loading = ({ ...rest }) => (
  <Flex
    flexDirection="column"
    alignItems="center"
    width={1}
    justifyContent="center"
    p={4}
    {...rest}
  >
    <Box width="48px" mx="auto">
      <Spinner color="black" size={48} />
    </Box>
    <Box py={4} textAlign="center">
      <Type fontWeight="500" fontSize={2} opacity={0.5}>
        Fetching apps...
      </Type>
    </Box>
  </Flex>
)

const Content = ({ topApps, allApps, ...rest }) => {
  if (!allApps) {
    return null
  }
  return (
    <Box maxWidth={1280} width="100%" mx="auto" p={[1, 2, 4]} {...rest}>
      <AppsSection title="Top Apps" apps={topApps} limit={24} />
      {allApps
        .sort((a, b) => a.label.localeCompare(b.label))
        .map(category => {
          const apps = category.apps.sort((a, b) => a.name.localeCompare(b.name))
          return <AppsSection title={category.label} apps={apps} />
        })}
    </Box>
  )
}

const AppsSection = ({ title, apps, limit, category, ...rest }) => {
  let appsList = apps.filter(app => app.imgixImageUrl)
  if (limit) {
    appsList = appsList.filter((app, i) => i <= limit - 1)
  }
  if (category) {
    appsList = appsList.filter(app => app.category === category)
  }
  return (
    <Box
      width={1}
      mx="auto"
      p={4}
      mb={4}
      borderBottom="1px solid rgba(15,15,15,0.1)"
      {...rest}
    >
      <Box textAlign="center">

{title}

      </Box>
      <Flex pt={4} flexWrap="wrap" justifyContent={['center', 'space-between']}>
        {appsList.map(app => (
          <AppItem
            key={app.name}
            name={app.name}
            imgixImageUrl={app.imgixImageUrl}
            website={app.website}
            description={app.description}
          />
        ))}
      </Flex>
    </Box>
  )
}

const Image = props => (
  <Box
    is="img"
    borderRadius="15px"
    boxShadow="1px 0px 18px 0px rgba(0, 0, 0, 0.1)"
    bg="white"
    {...props}
  />
)

const AppItem = ({ website, imgixImageUrl, name, description }) => {
  const descMaxLength = 55
  let desc = description.trim()

  if (description[description.length - 1] !== '.') {
    desc += '.'
  }

  if (description.length > descMaxLength) {
    desc = <>{desc.substring(0, descMaxLength).trim()}…</>
  }

  return (
    <Hover>
      {({ bind, hovered }) => (
        <Flex
          width={['calc(50% - 10px)', 1 / 3, 1 / 3, 1 / 4]}
          maxWidth={['160px', 'unset', 'unset', 'unset']}
          pr={[0, 3, 3, 4]}
          mb={5}
          flexShrink={0}
          flexGrow="1"
          alignSelf="stretch"
          transition="0.15s all ease-in-out"
          transform={hovered ? 'translateY(-5px)' : 'none'}
          color="blue.dark"
          {...bind}
        >
          <Flex
            maxWidth="100%"
            width={1}
            is="a"
            flexGrow={1}
            href={website}
            target="_blank"
            alignItems="center"
            flexDirection={['column', 'row', 'row', 'row']}
            textAlign={['center', 'left', 'left', 'left']}
            style={{
              textDecoration: 'none',
              color: 'black !important'
            }}
          >
            <Image
              maxWidth="100%"
              size="64px"
              display="block"
              flexShrink="0"
              src={imgixImageUrl}
              alt={name}
            />
            <Box ml={[0, 3, 3, 3]} mt={[3, 0, 0, 0]}>
              <Box>
                <Type
                  is="h3"
                  fontSize={[2]}
                  lineHeight={1.45}
                  fontWeight="600"
                  color="black !important"
                  mb={1}
                >
                  {name}
                </Type>
              </Box>
              <Box
                opacity={0.75}
                color="black !important"
                style={{
                  hyphens: 'auto'
                }}
              >
                {desc}
              </Box>
            </Box>
          </Flex>
        </Flex>
      )}
    </Hover>
  )
}

class HomeScreenPage extends React.Component {
  state = {
    tracked: false
  }
  handleTrack = () => {
    if (!this.state.tracked) {
      trackEventOnce('Browser Data Fetch')
      this.setState({ tracked: true })
    }
  }
  componentDidMount() {
    this.handleTrack()
  }
  render() {
    const loading =
      this.props.apps &&
      this.props.apps.loading
    return (
      <Box>
        <Navbar hideBackToHomeLink activeTab="home" />
        <Box className="home-screen">
          {loading ? (
            <Loading />
          ) : (
            <Content
              allApps={!loading && this.props.apps.appsByCategory}
              topApps={!loading && this.props.apps.topApps}
            />
          )}
        </Box>
      </Box>
    )
  }
}

Content.propTypes = {
  topApps: PropTypes.array.isRequired,
  allApps: PropTypes.array.isRequired
}

AppsSection.propTypes = {
  title: PropTypes.string.isRequired,
  apps: PropTypes.array.isRequired,
  limit: PropTypes.number.isRequired,
  category: PropTypes.string.isRequired
}
AppItem.propTypes = {
  website: PropTypes.string.isRequired,
  imgixImageUrl: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired
}

HomeScreenPage.propTypes = {
  apps: PropTypes.object.isRequired,
  refreshAppList: PropTypes.func.isRequired,
  doFetchApps: PropTypes.func.isRequired,
  appListLastUpdated: PropTypes.number,
  api: PropTypes.object.isRequired,
  instanceIdentifier: PropTypes.string,
  instanceCreationDate: PropTypes.number
}

const mapStateToProps = state => ({
  apps: state.apps,
  appListLastUpdated: state.apps.lastUpdated,
  api: state.settings.api,
  instanceIdentifier: state.apps.instanceIdentifier,
  instanceCreationDate: state.apps.instanceCreationDate
})

const mapDispatchToProps = dispatch =>
  bindActionCreators({ ...AppsActions }, dispatch)

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomeScreenPage)
<commit_msg>Safer checking for state.apps.topApps.length<commit_after>import PropTypes from 'prop-types'
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import Navbar from './components/Navbar'
import { AppsActions } from './store/apps'
import { Box, Flex, Type } from 'blockstack-ui'
import { Hover } from 'react-powerplug'
import { Spinner } from '@components/ui/components/spinner'
import { trackEventOnce } from '@utils/server-utils'

const Loading = ({ ...rest }) => (
  <Flex
    flexDirection="column"
    alignItems="center"
    width={1}
    justifyContent="center"
    p={4}
    {...rest}
  >
    <Box width="48px" mx="auto">
      <Spinner color="black" size={48} />
    </Box>
    <Box py={4} textAlign="center">
      <Type fontWeight="500" fontSize={2} opacity={0.5}>
        Fetching apps...
      </Type>
    </Box>
  </Flex>
)

const Content = ({ topApps, allApps, ...rest }) => {
  if (!allApps) {
    return null
  }
  return (
    <Box maxWidth={1280} width="100%" mx="auto" p={[1, 2, 4]} {...rest}>
      <AppsSection title="Top Apps" apps={topApps} limit={24} />
      {allApps
        .sort((a, b) => a.label.localeCompare(b.label))
        .map(category => {
          const apps = category.apps.sort((a, b) => a.name.localeCompare(b.name))
          return <AppsSection title={category.label} apps={apps} />
        })}
    </Box>
  )
}

const AppsSection = ({ title, apps, limit, category, ...rest }) => {
  let appsList = apps.filter(app => app.imgixImageUrl)
  if (limit) {
    appsList = appsList.filter((app, i) => i <= limit - 1)
  }
  if (category) {
    appsList = appsList.filter(app => app.category === category)
  }
  return (
    <Box
      width={1}
      mx="auto"
      p={4}
      mb={4}
      borderBottom="1px solid rgba(15,15,15,0.1)"
      {...rest}
    >
      <Box textAlign="center">

{title}

      </Box>
      <Flex pt={4} flexWrap="wrap" justifyContent={['center', 'space-between']}>
        {appsList.map(app => (
          <AppItem
            key={app.name}
            name={app.name}
            imgixImageUrl={app.imgixImageUrl}
            website={app.website}
            description={app.description}
          />
        ))}
      </Flex>
    </Box>
  )
}

const Image = props => (
  <Box
    is="img"
    borderRadius="15px"
    boxShadow="1px 0px 18px 0px rgba(0, 0, 0, 0.1)"
    bg="white"
    {...props}
  />
)

const AppItem = ({ website, imgixImageUrl, name, description }) => {
  const descMaxLength = 55
  let desc = description.trim()

  if (description[description.length - 1] !== '.') {
    desc += '.'
  }

  if (description.length > descMaxLength) {
    desc = <>{desc.substring(0, descMaxLength).trim()}…</>
  }

  return (
    <Hover>
      {({ bind, hovered }) => (
        <Flex
          width={['calc(50% - 10px)', 1 / 3, 1 / 3, 1 / 4]}
          maxWidth={['160px', 'unset', 'unset', 'unset']}
          pr={[0, 3, 3, 4]}
          mb={5}
          flexShrink={0}
          flexGrow="1"
          alignSelf="stretch"
          transition="0.15s all ease-in-out"
          transform={hovered ? 'translateY(-5px)' : 'none'}
          color="blue.dark"
          {...bind}
        >
          <Flex
            maxWidth="100%"
            width={1}
            is="a"
            flexGrow={1}
            href={website}
            target="_blank"
            alignItems="center"
            flexDirection={['column', 'row', 'row', 'row']}
            textAlign={['center', 'left', 'left', 'left']}
            style={{
              textDecoration: 'none',
              color: 'black !important'
            }}
          >
            <Image
              maxWidth="100%"
              size="64px"
              display="block"
              flexShrink="0"
              src={imgixImageUrl}
              alt={name}
            />
            <Box ml={[0, 3, 3, 3]} mt={[3, 0, 0, 0]}>
              <Box>
                <Type
                  is="h3"
                  fontSize={[2]}
                  lineHeight={1.45}
                  fontWeight="600"
                  color="black !important"
                  mb={1}
                >
                  {name}
                </Type>
              </Box>
              <Box
                opacity={0.75}
                color="black !important"
                style={{
                  hyphens: 'auto'
                }}
              >
                {desc}
              </Box>
            </Box>
          </Flex>
        </Flex>
      )}
    </Hover>
  )
}

class HomeScreenPage extends React.Component {
  state = {
    tracked: false
  }
  handleTrack = () => {
    if (!this.state.tracked) {
      trackEventOnce('Browser Data Fetch')
      this.setState({ tracked: true })
    }
  }
  componentDidMount() {
    this.handleTrack()
  }
  render() {
    const loading =
      this.props.apps &&
      this.props.apps.loading ||
      !this.props.apps.topApps ||
      !this.props.apps.topApps.length
    return (
      <Box>
        <Navbar hideBackToHomeLink activeTab="home" />
        <Box className="home-screen">
          {loading ? (
            <Loading />
          ) : (
            <Content
              allApps={!loading && this.props.apps.appsByCategory}
              topApps={!loading && this.props.apps.topApps}
            />
          )}
        </Box>
      </Box>
    )
  }
}

Content.propTypes = {
  topApps: PropTypes.array.isRequired,
  allApps: PropTypes.array.isRequired
}

AppsSection.propTypes = {
  title: PropTypes.string.isRequired,
  apps: PropTypes.array.isRequired,
  limit: PropTypes.number.isRequired,
  category: PropTypes.string.isRequired
}
AppItem.propTypes = {
  website: PropTypes.string.isRequired,
  imgixImageUrl: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired
}

HomeScreenPage.propTypes = {
  apps: PropTypes.object.isRequired,
  refreshAppList: PropTypes.func.isRequired,
  doFetchApps: PropTypes.func.isRequired,
  appListLastUpdated: PropTypes.number,
  api: PropTypes.object.isRequired,
  instanceIdentifier: PropTypes.string,
  instanceCreationDate: PropTypes.number
}

const mapStateToProps = state => ({
  apps: state.apps,
  appListLastUpdated: state.apps.lastUpdated,
  api: state.settings.api,
  instanceIdentifier: state.apps.instanceIdentifier,
  instanceCreationDate: state.apps.instanceCreationDate
})

const mapDispatchToProps = dispatch =>
  bindActionCreators({ ...AppsActions }, dispatch)

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomeScreenPage)
<|endoftext|>


<commit_before>const RedisManager = require('./RedisManager')
const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
const DocumentManager = require('./DocumentManager')
const HistoryManager = require('./HistoryManager')
const async = require('async')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const Errors = require('./Errors')

module.exports = {
  flushProjectWithLocks(projectId, _callback) {
    const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(projectId, function (error, docIds) {
      if (error) {
        return callback(error)
      }
      const errors = []
      const jobs = docIds.map((docId) => (callback) => {
        DocumentManager.flushDocIfLoadedWithLock(projectId, docId, function (
          error
        ) {
          if (error instanceof Errors.NotFoundError) {
            logger.warn(
              { err: error, projectId, docId },
              'found deleted doc when flushing'
            )
            callback()
          } else if (error) {
            logger.error({ err: error, projectId, docId }, 'error flushing doc')
            errors.push(error)
            callback()
          } else {
            callback()
          }
        })
      })

      logger.log({ projectId, docIds }, 'flushing docs')
      async.series(jobs, function () {
        if (errors.length > 0) {
          callback(new Error('Errors flushing docs. See log for details'))
        } else {
          callback(null)
        }
      })
    })
  },

  flushAndDeleteProjectWithLocks(projectId, options, _callback) {
    const timer = new Metrics.Timer(
      'projectManager.flushAndDeleteProjectWithLocks'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(projectId, function (error, docIds) {
      if (error) {
        return callback(error)
      }
      const errors = []
      const jobs = docIds.map((docId) => (callback) => {
        DocumentManager.flushAndDeleteDocWithLock(
          projectId,
          docId,
          {},
          function (error) {
            if (error) {
              logger.error(
                { err: error, projectId, docId },
                'error deleting doc'
              )
              errors.push(error)
            }
            callback()
          }
        )
      })

      logger.log({ projectId, docIds }, 'deleting docs')
      async.series(jobs, () =>
        // When deleting the project here we want to ensure that project
        // history is completely flushed because the project may be
        // deleted in web after this call completes, and so further
        // attempts to flush would fail after that.
        HistoryManager.flushProjectChanges(projectId, options, function (
          error
        ) {
          if (errors.length > 0) {
            callback(new Error('Errors deleting docs. See log for details'))
          } else if (error) {
            callback(error)
          } else {
            callback(null)
          }
        })
      )
    })
  },

  queueFlushAndDeleteProject(projectId, callback) {
    RedisManager.queueFlushAndDeleteProject(projectId, function (error) {
      if (error) {
        logger.error(
          { projectId, error },
          'error adding project to flush and delete queue'
        )
        return callback(error)
      }
      Metrics.inc('queued-delete')
      callback()
    })
  },

  getProjectDocsTimestamps(projectId, callback) {
    RedisManager.getDocIdsInProject(projectId, function (error, docIds) {
      if (error) {
        return callback(error)
      }
      if (docIds.length === 0) {
        return callback(null, [])
      }
      RedisManager.getDocTimestamps(docIds, function (error, timestamps) {
        if (error) {
          return callback(error)
        }
        callback(null, timestamps)
      })
    })
  },

  getProjectDocsAndFlushIfOld(
    projectId,
    projectStateHash,
    excludeVersions,
    _callback
  ) {
    const timer = new Metrics.Timer(
      'projectManager.getProjectDocsAndFlushIfOld'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.checkOrSetProjectState(projectId, projectStateHash, function (
      error,
      projectStateChanged
    ) {
      if (error) {
        logger.error(
          { err: error, projectId },
          'error getting/setting project state in getProjectDocsAndFlushIfOld'
        )
        return callback(error)
      }
      // we can't return docs if project structure has changed
      if (projectStateChanged) {
        return callback(
          Errors.ProjectStateChangedError('project state changed')
        )
      }
      // project structure hasn't changed, return doc content from redis
      RedisManager.getDocIdsInProject(projectId, function (error, docIds) {
        if (error) {
          logger.error(
            { err: error, projectId },
            'error getting doc ids in getProjectDocs'
          )
          return callback(error)
        }
        // get the doc lines from redis
        const jobs = docIds.map((docId) => (cb) => {
          DocumentManager.getDocAndFlushIfOldWithLock(
            projectId,
            docId,
            function (err, lines, version) {
              if (err) {
                logger.error(
                  { err, projectId, docId },
                  'error getting project doc lines in getProjectDocsAndFlushIfOld'
                )
                return cb(err)
              }
              const doc = { _id: docId, lines, v: version } // create a doc object to return
              cb(null, doc)
            }
          )
        })
        async.series(jobs, function (error, docs) {
          if (error) {
            return callback(error)
          }
          callback(null, docs)
        })
      })
    })
  },

  clearProjectState(projectId, callback) {
    RedisManager.clearProjectState(projectId, callback)
  },

  updateProjectWithLocks(
    projectId,
    projectHistoryId,
    userId,
    docUpdates,
    fileUpdates,
    version,
    _callback
  ) {
    const timer = new Metrics.Timer('projectManager.updateProject')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    const projectVersion = version
    let projectSubversion = 0 // project versions can have multiple operations

    let projectOpsLength = 0

    const handleDocUpdate = function (projectUpdate, cb) {
      const docId = projectUpdate.id
      projectUpdate.version = `${projectVersion}.${projectSubversion++}`
      if (projectUpdate.docLines != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          projectId,
          projectHistoryId,
          'doc',
          docId,
          userId,
          projectUpdate,
          function (error, count) {
            projectOpsLength = count
            cb(error)
          }
        )
      } else {
        DocumentManager.renameDocWithLock(
          projectId,
          docId,
          userId,
          projectUpdate,
          projectHistoryId,
          function (error, count) {
            projectOpsLength = count
            cb(error)
          }
        )
      }
    }

    const handleFileUpdate = function (projectUpdate, cb) {
      const fileId = projectUpdate.id
      projectUpdate.version = `${projectVersion}.${projectSubversion++}`
      if (projectUpdate.url != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          projectId,
          projectHistoryId,
          'file',
          fileId,
          userId,
          projectUpdate,
          function (error, count) {
            projectOpsLength = count
            cb(error)
          }
        )
      } else {
        ProjectHistoryRedisManager.queueRenameEntity(
          projectId,
          projectHistoryId,
          'file',
          fileId,
          userId,
          projectUpdate,
          function (error, count) {
            projectOpsLength = count
            cb(error)
          }
        )
      }
    }

    async.eachSeries(docUpdates, handleDocUpdate, function (error) {
      if (error) {
        return callback(error)
      }
      async.eachSeries(fileUpdates, handleFileUpdate, function (error) {
        if (error) {
          return callback(error)
        }
        if (
          HistoryManager.shouldFlushHistoryOps(
            projectOpsLength,
            docUpdates.length + fileUpdates.length,
            HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
          )
        ) {
          HistoryManager.flushProjectChangesAsync(projectId)
        }
        callback()
      })
    })
  },
}
<commit_msg>Decaf cleanup: use arrow functions for callbacks<commit_after>const RedisManager = require('./RedisManager')
const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
const DocumentManager = require('./DocumentManager')
const HistoryManager = require('./HistoryManager')
const async = require('async')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const Errors = require('./Errors')

module.exports = {
  flushProjectWithLocks(projectId, _callback) {
    const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
      if (error) {
        return callback(error)
      }
      const errors = []
      const jobs = docIds.map((docId) => (callback) => {
        DocumentManager.flushDocIfLoadedWithLock(projectId, docId, (error) => {
          if (error instanceof Errors.NotFoundError) {
            logger.warn(
              { err: error, projectId, docId },
              'found deleted doc when flushing'
            )
            callback()
          } else if (error) {
            logger.error({ err: error, projectId, docId }, 'error flushing doc')
            errors.push(error)
            callback()
          } else {
            callback()
          }
        })
      })

      logger.log({ projectId, docIds }, 'flushing docs')
      async.series(jobs, () => {
        if (errors.length > 0) {
          callback(new Error('Errors flushing docs. See log for details'))
        } else {
          callback(null)
        }
      })
    })
  },

  flushAndDeleteProjectWithLocks(projectId, options, _callback) {
    const timer = new Metrics.Timer(
      'projectManager.flushAndDeleteProjectWithLocks'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
      if (error) {
        return callback(error)
      }
      const errors = []
      const jobs = docIds.map((docId) => (callback) => {
        DocumentManager.flushAndDeleteDocWithLock(
          projectId,
          docId,
          {},
          (error) => {
            if (error) {
              logger.error(
                { err: error, projectId, docId },
                'error deleting doc'
              )
              errors.push(error)
            }
            callback()
          }
        )
      })

      logger.log({ projectId, docIds }, 'deleting docs')
      async.series(jobs, () =>
        // When deleting the project here we want to ensure that project
        // history is completely flushed because the project may be
        // deleted in web after this call completes, and so further
        // attempts to flush would fail after that.
        HistoryManager.flushProjectChanges(projectId, options, (error) => {
          if (errors.length > 0) {
            callback(new Error('Errors deleting docs. See log for details'))
          } else if (error) {
            callback(error)
          } else {
            callback(null)
          }
        })
      )
    })
  },

  queueFlushAndDeleteProject(projectId, callback) {
    RedisManager.queueFlushAndDeleteProject(projectId, (error) => {
      if (error) {
        logger.error(
          { projectId, error },
          'error adding project to flush and delete queue'
        )
        return callback(error)
      }
      Metrics.inc('queued-delete')
      callback()
    })
  },

  getProjectDocsTimestamps(projectId, callback) {
    RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
      if (error) {
        return callback(error)
      }
      if (docIds.length === 0) {
        return callback(null, [])
      }
      RedisManager.getDocTimestamps(docIds, (error, timestamps) => {
        if (error) {
          return callback(error)
        }
        callback(null, timestamps)
      })
    })
  },

  getProjectDocsAndFlushIfOld(
    projectId,
    projectStateHash,
    excludeVersions,
    _callback
  ) {
    const timer = new Metrics.Timer(
      'projectManager.getProjectDocsAndFlushIfOld'
    )
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    RedisManager.checkOrSetProjectState(
      projectId,
      projectStateHash,
      (error, projectStateChanged) => {
        if (error) {
          logger.error(
            { err: error, projectId },
            'error getting/setting project state in getProjectDocsAndFlushIfOld'
          )
          return callback(error)
        }
        // we can't return docs if project structure has changed
        if (projectStateChanged) {
          return callback(
            Errors.ProjectStateChangedError('project state changed')
          )
        }
        // project structure hasn't changed, return doc content from redis
        RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
          if (error) {
            logger.error(
              { err: error, projectId },
              'error getting doc ids in getProjectDocs'
            )
            return callback(error)
          }
          // get the doc lines from redis
          const jobs = docIds.map((docId) => (cb) => {
            DocumentManager.getDocAndFlushIfOldWithLock(
              projectId,
              docId,
              (err, lines, version) => {
                if (err) {
                  logger.error(
                    { err, projectId, docId },
                    'error getting project doc lines in getProjectDocsAndFlushIfOld'
                  )
                  return cb(err)
                }
                const doc = { _id: docId, lines, v: version } // create a doc object to return
                cb(null, doc)
              }
            )
          })
          async.series(jobs, (error, docs) => {
            if (error) {
              return callback(error)
            }
            callback(null, docs)
          })
        })
      }
    )
  },

  clearProjectState(projectId, callback) {
    RedisManager.clearProjectState(projectId, callback)
  },

  updateProjectWithLocks(
    projectId,
    projectHistoryId,
    userId,
    docUpdates,
    fileUpdates,
    version,
    _callback
  ) {
    const timer = new Metrics.Timer('projectManager.updateProject')
    const callback = function (...args) {
      timer.done()
      _callback(...args)
    }

    const projectVersion = version
    let projectSubversion = 0 // project versions can have multiple operations

    let projectOpsLength = 0

    const handleDocUpdate = function (projectUpdate, cb) {
      const docId = projectUpdate.id
      projectUpdate.version = `${projectVersion}.${projectSubversion++}`
      if (projectUpdate.docLines != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          projectId,
          projectHistoryId,
          'doc',
          docId,
          userId,
          projectUpdate,
          (error, count) => {
            projectOpsLength = count
            cb(error)
          }
        )
      } else {
        DocumentManager.renameDocWithLock(
          projectId,
          docId,
          userId,
          projectUpdate,
          projectHistoryId,
          (error, count) => {
            projectOpsLength = count
            cb(error)
          }
        )
      }
    }

    const handleFileUpdate = function (projectUpdate, cb) {
      const fileId = projectUpdate.id
      projectUpdate.version = `${projectVersion}.${projectSubversion++}`
      if (projectUpdate.url != null) {
        ProjectHistoryRedisManager.queueAddEntity(
          projectId,
          projectHistoryId,
          'file',
          fileId,
          userId,
          projectUpdate,
          (error, count) => {
            projectOpsLength = count
            cb(error)
          }
        )
      } else {
        ProjectHistoryRedisManager.queueRenameEntity(
          projectId,
          projectHistoryId,
          'file',
          fileId,
          userId,
          projectUpdate,
          (error, count) => {
            projectOpsLength = count
            cb(error)
          }
        )
      }
    }

    async.eachSeries(docUpdates, handleDocUpdate, (error) => {
      if (error) {
        return callback(error)
      }
      async.eachSeries(fileUpdates, handleFileUpdate, (error) => {
        if (error) {
          return callback(error)
        }
        if (
          HistoryManager.shouldFlushHistoryOps(
            projectOpsLength,
            docUpdates.length + fileUpdates.length,
            HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
          )
        ) {
          HistoryManager.flushProjectChangesAsync(projectId)
        }
        callback()
      })
    })
  },
}
<|endoftext|>


<commit_before>"use strict";

angular.module('arethusa.sg').service('sg', [
  'state',
  'configurator',
  function(state, configurator) {
    var self = this;
    var retriever;
    this.labelAs = "long";
    this.defineAncestors = true;

    function configure() {
      configurator.getConfAndDelegate('sg', self, ['labels']);
      retriever = configurator.getRetriever(self.conf.retriever);
    }

    configure();

    function sgTemplate() {
      return {
        morph: {},
        ancestors: [],
        definingAttrs: [],
        menu: {}
      };
    }

    function grammarReset(grammar) {
      grammar.ancestors = [];
      grammar.definingAttrs = [];
      grammar.sbsNested = [];
    }

    function createInternalState() {
      return arethusaUtil.inject({}, state.tokens, function(memo, id, token) {
        var grammar = sgTemplate();
        var morph = token.morphology || {};
        grammar.string = token.string;
        checkAndUpdateGrammar(morph, grammar);
        memo[id] = grammar;
      });
    }

    this.currentGrammar = function() {
      return arethusaUtil.inject({}, state.selectedTokens, function(memo, id, event) {
        var morph = state.tokens[id].morphology || {};
        var grammar = self.grammar[id];
        checkAndUpdateGrammar(morph, grammar);
        memo[id] = grammar;
      });
    };

    function morphHasChanged(a, b) {
      return !angular.equals(a, b);
    }

    function checkAndUpdateGrammar(morph, grammar) {
      if (morphHasChanged(grammar.morph, morph.attributes)) {
        angular.extend(grammar.morph, morph.attributes);
        updateGrammar(self.labels, grammar);
      }
      return grammar;
    }

    function updateGrammar(labels, grammar) {
      grammarReset(grammar);
      findDefiningAttributes(self.labels, grammar, grammar.definingAttrs);
      extractMenu(grammar);
    }

    function findDefiningAttributes(labels, grammar, target) {
      arethusaUtil.inject(target, labels, function(memo, label, val) {
        var dep = val.dependency;
        if (dep) {
          var morph = grammar.morph;
          var nextLevel;
          angular.forEach(dep, function(depVal, depCat) {
            // More a hack than a solution so far, through
            // the RegExp we can match "1st2nd3rd" of the
            // pers dep. So far all works, but the RegExp
            // could cause trouble, when wrong dependency
            // values match...
            if (dependencyMet(morph[depCat], depVal)) {
              val = angular.copy(val);
              memo.push(val);
              nextLevel = val.nested || {};
              angular.forEach(nextLevel, function(nestedMenu, nestedLabel) {
                if (nestedMenu.nestedDependency) {
                  var nextNestedLevel = [];
                  findDefiningAttributes(nestedMenu.nested, grammar, nextNestedLevel);
                  nestedMenu.nested = { nested: nextNestedLevel.pop() };
                }
              });
              findDefiningAttributes(nextLevel, grammar, target);
            }
          });
        }
      });
    }

    function dependencyMet(morphValue, depValue) {
      if (angular.isDefined(morphValue)) {
        return morphValue === depValue || depValue === "*";
      }
    }

    // We already captured the defining attributes at this point - they
    // are all stored as full objects with their full nested structure.
    // The menu we want to present to the user is therefore the last one
    // in this array structure.
    function extractMenu(grammar) {
      var attrs = grammar.definingAttrs;
      // Could be that this array is empty!
      var lastAttr = attrs[attrs.length - 1] || {};
      grammar.menu = lastAttr.nested || {};
    }

    function propagateToState() {
      angular.forEach(self.grammar, function(val, id) {
        state.tokens[id].sg = val;
      });
    }

    this.requestGrammar = function(sections, callback) {
      retriever.getData(sections, callback);
    };

    this.init = function() {
      configure();
      self.grammar = createInternalState();
      self.readerRequested = false;
      propagateToState();
    };
  }
]);
<commit_msg>Remove obsolete codepiece in sg<commit_after>"use strict";

angular.module('arethusa.sg').service('sg', [
  'state',
  'configurator',
  function(state, configurator) {
    var self = this;
    var retriever;
    this.labelAs = "long";
    this.defineAncestors = true;

    function configure() {
      configurator.getConfAndDelegate('sg', self, ['labels']);
      retriever = configurator.getRetriever(self.conf.retriever);
    }

    configure();

    function sgTemplate() {
      return {
        morph: {},
        ancestors: [],
        definingAttrs: [],
        menu: {}
      };
    }

    function grammarReset(grammar) {
      grammar.ancestors = [];
      grammar.definingAttrs = [];
    }

    function createInternalState() {
      return arethusaUtil.inject({}, state.tokens, function(memo, id, token) {
        var grammar = sgTemplate();
        var morph = token.morphology || {};
        grammar.string = token.string;
        checkAndUpdateGrammar(morph, grammar);
        memo[id] = grammar;
      });
    }

    this.currentGrammar = function() {
      return arethusaUtil.inject({}, state.selectedTokens, function(memo, id, event) {
        var morph = state.tokens[id].morphology || {};
        var grammar = self.grammar[id];
        checkAndUpdateGrammar(morph, grammar);
        memo[id] = grammar;
      });
    };

    function morphHasChanged(a, b) {
      return !angular.equals(a, b);
    }

    function checkAndUpdateGrammar(morph, grammar) {
      if (morphHasChanged(grammar.morph, morph.attributes)) {
        angular.extend(grammar.morph, morph.attributes);
        updateGrammar(self.labels, grammar);
      }
      return grammar;
    }

    function updateGrammar(labels, grammar) {
      grammarReset(grammar);
      findDefiningAttributes(self.labels, grammar, grammar.definingAttrs);
      extractMenu(grammar);
    }

    function findDefiningAttributes(labels, grammar, target) {
      arethusaUtil.inject(target, labels, function(memo, label, val) {
        var dep = val.dependency;
        if (dep) {
          var morph = grammar.morph;
          var nextLevel;
          angular.forEach(dep, function(depVal, depCat) {
            // More a hack than a solution so far, through
            // the RegExp we can match "1st2nd3rd" of the
            // pers dep. So far all works, but the RegExp
            // could cause trouble, when wrong dependency
            // values match...
            if (dependencyMet(morph[depCat], depVal)) {
              val = angular.copy(val);
              memo.push(val);
              nextLevel = val.nested || {};
              angular.forEach(nextLevel, function(nestedMenu, nestedLabel) {
                if (nestedMenu.nestedDependency) {
                  var nextNestedLevel = [];
                  findDefiningAttributes(nestedMenu.nested, grammar, nextNestedLevel);
                  nestedMenu.nested = { nested: nextNestedLevel.pop() };
                }
              });
              findDefiningAttributes(nextLevel, grammar, target);
            }
          });
        }
      });
    }

    function dependencyMet(morphValue, depValue) {
      if (angular.isDefined(morphValue)) {
        return morphValue === depValue || depValue === "*";
      }
    }

    // We already captured the defining attributes at this point - they
    // are all stored as full objects with their full nested structure.
    // The menu we want to present to the user is therefore the last one
    // in this array structure.
    function extractMenu(grammar) {
      var attrs = grammar.definingAttrs;
      // Could be that this array is empty!
      var lastAttr = attrs[attrs.length - 1] || {};
      grammar.menu = lastAttr.nested || {};
    }

    function propagateToState() {
      angular.forEach(self.grammar, function(val, id) {
        state.tokens[id].sg = val;
      });
    }

    this.requestGrammar = function(sections, callback) {
      retriever.getData(sections, callback);
    };

    this.init = function() {
      configure();
      self.grammar = createInternalState();
      self.readerRequested = false;
      propagateToState();
    };
  }
]);
<|endoftext|>


<commit_before>'use strict';

let icons = {};
const fs = require('fs');

function load() {
	fs.readFile('config/icons.json', 'utf8', function (err, file) {
		if (err) return;
		icons = JSON.parse(file);
	});
}
load();

function updateIcons() {
	fs.writeFileSync('config/icons.json', JSON.stringify(icons));

	let newCss = '/* ICONS START */\n';

	for (let name in icons) {
		newCss += generateCSS(name, icons[name]);
	}
	newCss += '/* ICONS END */\n';

	let file = fs.readFileSync('config/custom.css', 'utf8').split('\n');
	if (~file.indexOf('/* ICONS START */')) file.splice(file.indexOf('/* ICONS START */'), (file.indexOf('/* ICONS END */') - file.indexOf('/* ICONS START */')) + 1);
	fs.writeFileSync('config/custom.css', file.join('\n') + newCss);
	Wisp.reloadCSS();
}
Wisp.updateIcons = updateIcons;

function generateCSS(name, icon) {
	let css = '';
	let rooms = [];
	name = toId(name);
	for (let room in Rooms.rooms) {
		if (Rooms.rooms[room].id === 'global' || Rooms.rooms[room].type !== 'chat' || Rooms.rooms[room].isPersonal) continue;
		if (!isNaN(Number(Rooms.rooms[room].id.charAt(0)))) continue;
		rooms.push('#' + Rooms.rooms[room].id + '-userlist-user-' + name);
	}
	css = rooms.join(', ');
	css += '{\nbackground: url("' + icon + '") no-repeat right\n}\n';
	return css;
}

exports.commands = {
	customicon: 'icon',
	icon: function (target, room, user) {
		if (!this.can('customicon')) return false;
		target = target.split(',');
		for (let u in target) target[u] = target[u].trim();
		if (!target[1]) return this.parse('/help icon');
		if (toId(target[0]).length > 19) return this.errorReply("Usernames are not this long...");
		if (target[1] === 'delete') {
			if (!icons[toId(target[0])]) return this.errorReply('/icon - ' + target[0] + ' does not have an icon.');
			delete icons[toId(target[0])];
			updateIcons();
			this.sendReply("You removed " + target[0] + "'s icon.");
			Rooms('upperstaff').add(user.name + " removed " + target[0] + "'s icon.").update();
			this.privateModCommand("(" + target[0] + "'s icon was removed by " + user.name + ".)");
			if (Users(target[0]) && Users(target[0]).connected) Users(target[0]).popup(user.name + " removed your icon.");
			return;
		}

		this.sendReply("|raw|You have given " + Tools.escapeHTML(target[0]) + " an icon.");
		Rooms('upperstaff').add('|raw|' + Tools.escapeHTML(target[0]) + ' has received an icon from ' + Tools.escapeHTML(user.name) + '.').update();
		this.privateModCommand("(" + target[0] + " has recieved icon: '" + target[1] + "' from " + user.name + ".)");
		icons[toId(target[0])] = target[1];
		updateIcons();
	},
	iconhelp: [
		"Commands Include:",
		"/icon [user], [image url] - Gives [user] an icon of [image url]",
		"/icon [user], delete - Deletes a user's icon",
	],
};
<commit_msg>Icons: add popup replies when set/removed<commit_after>'use strict';

let icons = {};
const fs = require('fs');

function load() {
	fs.readFile('config/icons.json', 'utf8', function (err, file) {
		if (err) return;
		icons = JSON.parse(file);
	});
}
load();

function updateIcons() {
	fs.writeFileSync('config/icons.json', JSON.stringify(icons));

	let newCss = '/* ICONS START */\n';

	for (let name in icons) {
		newCss += generateCSS(name, icons[name]);
	}
	newCss += '/* ICONS END */\n';

	let file = fs.readFileSync('config/custom.css', 'utf8').split('\n');
	if (~file.indexOf('/* ICONS START */')) file.splice(file.indexOf('/* ICONS START */'), (file.indexOf('/* ICONS END */') - file.indexOf('/* ICONS START */')) + 1);
	fs.writeFileSync('config/custom.css', file.join('\n') + newCss);
	Wisp.reloadCSS();
}
Wisp.updateIcons = updateIcons;

function generateCSS(name, icon) {
	let css = '';
	let rooms = [];
	name = toId(name);
	for (let room in Rooms.rooms) {
		if (Rooms.rooms[room].id === 'global' || Rooms.rooms[room].type !== 'chat' || Rooms.rooms[room].isPersonal) continue;
		if (!isNaN(Number(Rooms.rooms[room].id.charAt(0)))) continue;
		rooms.push('#' + Rooms.rooms[room].id + '-userlist-user-' + name);
	}
	css = rooms.join(', ');
	css += '{\nbackground: url("' + icon + '") no-repeat right\n}\n';
	return css;
}

exports.commands = {
	customicon: 'icon',
	icon: function (target, room, user) {
		if (!this.can('customicon')) return false;
		target = target.split(',');
		for (let u in target) target[u] = target[u].trim();
		if (!target[1]) return this.parse('/help icon');
		if (toId(target[0]).length > 19) return this.errorReply("Usernames are not this long...");
		if (target[1] === 'delete') {
			if (!icons[toId(target[0])]) return this.errorReply('/icon - ' + target[0] + ' does not have an icon.');
			delete icons[toId(target[0])];
			updateIcons();
			this.sendReply("You removed " + target[0] + "'s icon.");
			Rooms('upperstaff').add(user.name + " removed " + target[0] + "'s icon.").update();
			this.privateModCommand("(" + target[0] + "'s icon was removed by " + user.name + ".)");
			if (Users(target[0]) && Users(target[0]).connected) Users(target[0]).popup("|modal||html|" + Wisp.nameColor(user.name, true) + " removed your icon.");
			return;
		}

		this.sendReply("|raw|You have given " + Tools.escapeHTML(target[0]) + " an icon.");
		if (Users(target[0]) && Users(target[0]).connected) Users(target[0]).popup("|modal||html|" + Wisp.nameColor(user.name, true) + " has set your icon as : <img src='" + target[1] + "' width='32' height='32'");
		Rooms('upperstaff').add('|raw|' + Tools.escapeHTML(target[0]) + ' has received an icon from ' + Tools.escapeHTML(user.name) + '.').update();
		this.privateModCommand("(" + target[0] + " has recieved icon: '" + target[1] + "' from " + user.name + ".)");
		icons[toId(target[0])] = target[1];
		updateIcons();
	},
	iconhelp: [
		"Commands Include:",
		"/icon [user], [image url] - Gives [user] an icon of [image url]",
		"/icon [user], delete - Deletes a user's icon",
	],
};
<|endoftext|>


<commit_before>mol.modules.map.search = function(mol) {

    mol.map.search = {};

    mol.map.search.SearchEngine = mol.mvp.Engine.extend({
        /**
         * @param bus mol.bus.Bus
         */
        init: function(proxy, bus) {
            this.proxy = proxy;
            this.bus = bus;
            this.searching = {};
            this.names = [];
            this.seenHint = false;
            this.ac_label_html = ''+
                '
' +
                    '{0}' +
                    '{1}' +
                '
';
            this.ac_sql = "" +
                "SELECT n,v FROM ac WHERE n~*'\\m{0}' OR v~*'\\m{0}'";
            this.search_sql = '' +
                'SELECT DISTINCT l.scientificname as name,'+
                    't.type as type,'+
                    't.cartocss as css,' +
                    't.sort_order as type_sort_order, ' +
                    't.title as type_title, '+
                    't.opacity as opacity, ' +
                    'CONCAT(l.provider,\'\') as source, '+
                    'CONCAT(p.title,\'\') as source_title,'+
                    's.source_type as source_type, ' +
                    's.title as source_type_title, ' +
                    "CASE WHEN l.feature_count is not null THEN CASE WHEN d.type = 'taxogeooccchecklist' " +
                        'THEN ' +
                            "CONCAT("+
                                "to_char(l.occ_count,'999,999,999'),"+
                                "' records
'," +
                                "to_char(l.feature_count, '999,999,999'),"+
                                "' locations'"+
			    ") " +
                        'ELSE ' +
                            "CONCAT(to_char(l.feature_count,'999,999,999'),' features') "+
                    'END ELSE \'\' END as feature_count, '+
                    'CONCAT(n.v,\'\') as names, ' +
                    'CASE WHEN l.extent is null THEN null ELSE ' +
                    'CONCAT(\'{' +
                        '"sw":{' +
                            '"lng":\',ST_XMin(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\', '+
                            '"lat":\',ST_YMin(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\' '+
                        '}, '+
                        '"ne":{' +
                        '"lng":\',ST_XMax(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\', ' +
                        '"lat":\',ST_YMax(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\' ' +
                        '}}\') ' +
                    'END as extent, ' +
                    'l.dataset_id as dataset_id, ' +
                    'd.dataset_title as dataset_title, ' +
                    'd.style_table as style_table ' +

                'FROM layer_metadata l ' +
                'LEFT JOIN data_registry d ON ' +
                    'l.dataset_id = d.dataset_id ' +
                'LEFT JOIN types t ON ' +
                    'l.type = t.type ' +
                'LEFT JOIN providers p ON ' +
                    'l.provider = p.provider ' +
                'LEFT JOIN source_types s ON ' +
                    'p.source_type = s.source_type ' +
                'LEFT JOIN ac n ON ' +
                    'l.scientificname = n.n ' +
                'WHERE ' +
                     "n.n~*'\\m{0}' OR n.v~*'\\m{0}' " +
                'ORDER BY name, type_sort_order';
        },

        /**
         * Starts the SearchEngine. Note that the container parameter is
         * ignored.
         */
        start: function() {
            this.display = new mol.map.search.SearchDisplay();
            this.display.toggle(true);
            this.initAutocomplete();
            this.addEventHandlers();
            this.fireEvents();
        },
        /*
         * Initialize autocomplate functionality
         */
        initAutocomplete: function() {
            this.populateAutocomplete(null, null);

            //http://stackoverflow.com/questions/2435964/jqueryui-how-can-i-custom-format-the-autocomplete-plug-in-results
            $.ui.autocomplete.prototype._renderItem = function (ul, item) {

                item.label = item.label.replace(
                    new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
                       $.ui.autocomplete.escapeRegex(this.term) +
                       ")(?![^<>]*>)(?![^&;]+;)", "gi"),
                    "$1"
                );
                return $("

")
                    .data("item.autocomplete", item)
                    .append("" + item.label + "")
                    .appendTo(ul);
            };
        },

        /*
         * Populate autocomplete results list
         */
        populateAutocomplete : function(action, response) {
            var self = this;
            $(this.display.searchBox).autocomplete(
                {
                    minLength: 3,
                    source: function(request, response) {
                        $.getJSON(
                            'http://mol.cartodb.com/api/v1/sql?q={0}'.format(
                                    self.ac_sql.format(
                                        $.trim(request.term)
                                            .replace(/ /g, ' ')
                                    )
                            ),
                            function (json) {
                                var names = [],scinames=[];
                                _.each (
                                    json.rows,
                                    function(row) {
                                        var sci, eng;
                                        if(row.n != undefined){
                                            sci = row.n;
                                            eng = (row.v == null ||
                                                row.v == '') ?
                                                    '' :
                                                    ', {0}'.format(
                                                        row.v.replace(
                                                            /'S/g, "'s"
                                                        )
                                                    );
                                            names.push({
                                                label:self.ac_label_html
                                                    .format(sci, eng),
                                                value:sci
                                            });
                                            scinames.push(sci);
                                       }
                                   }
                                );
                                if(scinames.length>0) {
                                    self.names=scinames;
                                }
                                response(names);
                                self.bus.fireEvent(
                                    new mol.bus.Event(
                                        'hide-loading-indicator',
                                        {source : "autocomplete"}
                                    )
                                );
                             },
                             'json'
                        );
                    },
                    select: function(event, ui) {
                        self.searching[ui.item.value] = false;
                        self.names = [ui.item.value];
                        self.search(ui.item.value);
                    },
                    close: function(event,ui) {

                    },
                    search: function(event, ui) {
                        self.searching[$(this).val()] = true;
                        self.names=[];
                        self.bus.fireEvent(
                            new mol.bus.Event(
                                'show-loading-indicator',
                                {source : "autocomplete"}
                            )
                        );
                    },
                    open: function(event, ui) {
                        self.searching[$(this).val()] = false;
                        self.bus.fireEvent(
                             new mol.bus.Event(
                                'hide-loading-indicator',
                                {source : "autocomplete"}
                            )
                        );
                    }
              });
        },

        addEventHandlers: function() {
            var self = this;

            /**
             * Callback that toggles the search display visibility. The
             * event is expected to have the following properties:
             *
             *   event.visible - true to show the display, false to hide it.
             *
             * @param event mol.bus.Event
             */
            this.bus.addHandler(
                'search-display-toggle',
                function(event) {
                    if(event.visible != true ) {
                        self.display.searchDisplay.hide();
                        self.display.find('.toggle').text('▶');
                    } else {

                        self.display.searchDisplay.show();
                        self.display.find('.toggle').text('◀');
                    }
                    self.bus.fireEvent(
                        new mol.bus.Event('results-display-toggle', {})
                    );

                }
            );
            this.bus.addHandler(
                'show-search-hint',
                function(event) {
                    if(!self.seenHint) {
                        self.display.qtip({
                            content: {
                                text: '' +
                                    '
' +
                                        'Type a species name here and select.' +
                                    '
'
                            },
                            style: { width: {min: 400, max:500}},
                            position: {
                                my: 'top left',
                                at: 'bottom right'
                            },
                            show: {
                                event: false,
                                ready: true
                            },
                            hide: {
                                fixed: false,
                                event: 'unfocus'
                            }
                        });
                        self.seenHint=true;
                    }
                }
            );
            this.bus.addHandler(
                'close-autocomplete',
                function(event) {
                    $(self.display.searchBox).autocomplete("close");
                }
            );
            this.bus.addHandler(
                'hide-search',
                function(event) {
                    $(self.display).hide();
                }
            );
            this.bus.addHandler(
                'show-search',
                function(event) {
                    $(self.display).show();
                }
            );
            this.bus.addHandler(
                'search',
                function(event) {
                    if (event.term != undefined) {
                        if (!self.display.is(':visible')) {
                            self.bus.fireEvent(
                                new mol.bus.Event(
                                    'search-display-toggle',
                                    {visible : true}
                                )
                            );
                        }

                        self.search(event.term);

                        if (self.display.searchBox.val()=='') {
                            self.display.searchBox.val(event.term);
                        }
                    }
               }
            );

            /**
             * Clicking the go button executes a search.
             */

            this.display.click(
                function(event) {
                    $(this).qtip("hide");

                }
            )

            this.display.goButton.click(
                function(event) {
                    self.userEnteredSearch(self.display.searchBox.val());
                }
            );

            /**
             * Clicking the cancel button hides the search display and fires
             * a cancel-search event on the bus.
             */
            this.display.toggleButton.click(
                function(event) {
                    var params = {
                        visible: false
                    }, that = this;

                    if(self.display.searchDisplay.is(':visible')) {
                        self.display.searchDisplay.hide();
                        $(this).text('▶');
                        params.visible = false;
                    } else {

                        self.display.searchDisplay.show();
                        $(this).text('◀');
                        params.visible = true;
                    }

                    self.bus.fireEvent(
                        new mol.bus.Event('results-display-toggle', params));
                }
            );

            /**
             * Pressing the return button clicks the go button.
             */
            this.display.searchBox.keyup(
                function(event) {
                    if (event.keyCode === 13) {
                        $(this).autocomplete("close");
                        self.bus.fireEvent(
                            new mol.bus.Event(
                                'hide-loading-indicator',
                                {source : "autocomplete"}
                            )
                        );
                        self.userEnteredSearch($(this).val());
                    }
                }
            );
        },

        /**
         * Fires the 'add-map-control' event. The mol.map.MapEngine handles
         * this event and adds the display to the map.
         */
        fireEvents: function() {
            var params = {
                    display: this.display,
                    slot: mol.map.ControlDisplay.Slot.TOP,
                    position: google.maps.ControlPosition.TOP_LEFT
                },
                event = new mol.bus.Event('add-map-control', params);

            this.bus.fireEvent(event);
        },

        /**
         * Searches CartoDB using a term from the search box.
         * This checks the user-entered search term before
         * passing it on to search().
         */
        userEnteredSearch: function(term) {
            var self = this;

            // Trim the term. Since we later overwrite the search string,
            // this trims that as well.
            term = $.trim(term);

            // Turn off autocomplete.
            $(self.display.searchBox).autocomplete('disable');
            $(self.display.searchBox).autocomplete('close');

            // Produce an error if it's too short.
            if(term.length < 3) {
                if (term.length == 0) {
                    self.bus.fireEvent(new mol.bus.Event('clear-results'));
                } else {
                    alert('' +
                        'Please enter at least 3 characters ' +
                        'in the search box.'
                     );
                }
                return;
            }

            // Clear results, otherwise the box just stays on the screen
            // and looks ugly.
            self.bus.fireEvent(new mol.bus.Event('clear-results'));

            // Now that the term is cleaned, go through the normal search
            // procedure.
            self.search(term);
        },

        /**
         * Searches CartoDB using a term from the search box.
         *
         * @param term the search term (scientific name)
         */
        search: function(term) {
            var self = this;

            // Show loading indicator.
            self.bus.fireEvent(
                new mol.bus.Event(
                    'show-loading-indicator',
                    {source : "search-{0}".format(term)}
                )
            );

            $.getJSON(
                'http://mol.cartodb.com/api/v1/sql?q={0}'.format(
                    this.search_sql.format(
                        $.trim(term)
                        .replace(/ /g, ' ')
                    )
                ),
                function (response) {
                    var results = {term:term, response:response};

                    self.bus.fireEvent(
                        new mol.bus.Event(
                            'hide-loading-indicator',
                            {source : "search-{0}".format(term)}
                        )
                    );
                    self.bus.fireEvent(
                        new mol.bus.Event(
                            'search-results',
                            results
                        )
                    );
                    $(self.display.searchBox).autocomplete('enable');
                }
            );

        }
    });

    mol.map.search.SearchDisplay = mol.mvp.View.extend({
        init: function() {
            var html = '' +
                '
' +
                '
Search
' +
                '
' +
                '       <input class="value ui-autocomplete-input" type="text" ' +
                            'placeholder="Search by species name">' +
                '       <button class="execute">Go</button>' +
                '
'+
                '   <button class="toggle">▶</button>' +
                '
';

            this._super(html);
            this.goButton = $(this).find('.execute');
            this.toggleButton = $(this).find('.toggle');
            this.searchDisplay = $(this).find('.searchDisplay');
            this.searchBox = $(this).find('.value');
        },

        clear: function() {
            this.searchBox.html('');
        }
    });
};
<commit_msg>Added an error callback on the CartoDB layer search so that synoynm counts are maintained correctly.<commit_after>mol.modules.map.search = function(mol) {

    mol.map.search = {};

    mol.map.search.SearchEngine = mol.mvp.Engine.extend({
        /**
         * @param bus mol.bus.Bus
         */
        init: function(proxy, bus) {
            this.proxy = proxy;
            this.bus = bus;
            this.searching = {};
            this.names = [];
            this.seenHint = false;
            this.ac_label_html = ''+
                '
' +
                    '{0}' +
                    '{1}' +
                '
';
            this.ac_sql = "" +
                "SELECT n,v FROM ac WHERE n~*'\\m{0}' OR v~*'\\m{0}'";
            this.search_sql = '' +
                'SELECT DISTINCT l.scientificname as name,'+
                    't.type as type,'+
                    't.cartocss as css,' +
                    't.sort_order as type_sort_order, ' +
                    't.title as type_title, '+
                    't.opacity as opacity, ' +
                    'CONCAT(l.provider,\'\') as source, '+
                    'CONCAT(p.title,\'\') as source_title,'+
                    's.source_type as source_type, ' +
                    's.title as source_type_title, ' +
                    "CASE WHEN l.feature_count is not null THEN CASE WHEN d.type = 'taxogeooccchecklist' " +
                        'THEN ' +
                            "CONCAT("+
                                "to_char(l.occ_count,'999,999,999'),"+
                                "' records
'," +
                                "to_char(l.feature_count, '999,999,999'),"+
                                "' locations'"+
			    ") " +
                        'ELSE ' +
                            "CONCAT(to_char(l.feature_count,'999,999,999'),' features') "+
                    'END ELSE \'\' END as feature_count, '+
                    'CONCAT(n.v,\'\') as names, ' +
                    'CASE WHEN l.extent is null THEN null ELSE ' +
                    'CONCAT(\'{' +
                        '"sw":{' +
                            '"lng":\',ST_XMin(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\', '+
                            '"lat":\',ST_YMin(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\' '+
                        '}, '+
                        '"ne":{' +
                        '"lng":\',ST_XMax(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\', ' +
                        '"lat":\',ST_YMax(box2d(ST_Transform(ST_SetSRID(l.extent,3857),4326))),\' ' +
                        '}}\') ' +
                    'END as extent, ' +
                    'l.dataset_id as dataset_id, ' +
                    'd.dataset_title as dataset_title, ' +
                    'd.style_table as style_table ' +

                'FROM layer_metadata l ' +
                'LEFT JOIN data_registry d ON ' +
                    'l.dataset_id = d.dataset_id ' +
                'LEFT JOIN types t ON ' +
                    'l.type = t.type ' +
                'LEFT JOIN providers p ON ' +
                    'l.provider = p.provider ' +
                'LEFT JOIN source_types s ON ' +
                    'p.source_type = s.source_type ' +
                'LEFT JOIN ac n ON ' +
                    'l.scientificname = n.n ' +
                'WHERE ' +
                     "n.n~*'\\m{0}' OR n.v~*'\\m{0}' " +
                'ORDER BY name, type_sort_order';
        },

        /**
         * Starts the SearchEngine. Note that the container parameter is
         * ignored.
         */
        start: function() {
            this.display = new mol.map.search.SearchDisplay();
            this.display.toggle(true);
            this.initAutocomplete();
            this.addEventHandlers();
            this.fireEvents();
        },
        /*
         * Initialize autocomplate functionality
         */
        initAutocomplete: function() {
            this.populateAutocomplete(null, null);

            //http://stackoverflow.com/questions/2435964/jqueryui-how-can-i-custom-format-the-autocomplete-plug-in-results
            $.ui.autocomplete.prototype._renderItem = function (ul, item) {

                item.label = item.label.replace(
                    new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
                       $.ui.autocomplete.escapeRegex(this.term) +
                       ")(?![^<>]*>)(?![^&;]+;)", "gi"),
                    "$1"
                );
                return $("

")
                    .data("item.autocomplete", item)
                    .append("" + item.label + "")
                    .appendTo(ul);
            };
        },

        /*
         * Populate autocomplete results list
         */
        populateAutocomplete : function(action, response) {
            var self = this;
            $(this.display.searchBox).autocomplete(
                {
                    minLength: 3,
                    source: function(request, response) {
                        $.getJSON(
                            'http://mol.cartodb.com/api/v1/sql?q={0}'.format(
                                    self.ac_sql.format(
                                        $.trim(request.term)
                                            .replace(/ /g, ' ')
                                    )
                            ),
                            function (json) {
                                var names = [],scinames=[];
                                _.each (
                                    json.rows,
                                    function(row) {
                                        var sci, eng;
                                        if(row.n != undefined){
                                            sci = row.n;
                                            eng = (row.v == null ||
                                                row.v == '') ?
                                                    '' :
                                                    ', {0}'.format(
                                                        row.v.replace(
                                                            /'S/g, "'s"
                                                        )
                                                    );
                                            names.push({
                                                label:self.ac_label_html
                                                    .format(sci, eng),
                                                value:sci
                                            });
                                            scinames.push(sci);
                                       }
                                   }
                                );
                                if(scinames.length>0) {
                                    self.names=scinames;
                                }
                                response(names);
                                self.bus.fireEvent(
                                    new mol.bus.Event(
                                        'hide-loading-indicator',
                                        {source : "autocomplete"}
                                    )
                                );
                             },
                             'json'
                        );
                    },
                    select: function(event, ui) {
                        self.searching[ui.item.value] = false;
                        self.names = [ui.item.value];
                        self.search(ui.item.value);
                    },
                    close: function(event,ui) {

                    },
                    search: function(event, ui) {
                        self.searching[$(this).val()] = true;
                        self.names=[];
                        self.bus.fireEvent(
                            new mol.bus.Event(
                                'show-loading-indicator',
                                {source : "autocomplete"}
                            )
                        );
                    },
                    open: function(event, ui) {
                        self.searching[$(this).val()] = false;
                        self.bus.fireEvent(
                             new mol.bus.Event(
                                'hide-loading-indicator',
                                {source : "autocomplete"}
                            )
                        );
                    }
              });
        },

        addEventHandlers: function() {
            var self = this;

            /**
             * Callback that toggles the search display visibility. The
             * event is expected to have the following properties:
             *
             *   event.visible - true to show the display, false to hide it.
             *
             * @param event mol.bus.Event
             */
            this.bus.addHandler(
                'search-display-toggle',
                function(event) {
                    if(event.visible != true ) {
                        self.display.searchDisplay.hide();
                        self.display.find('.toggle').text('▶');
                    } else {

                        self.display.searchDisplay.show();
                        self.display.find('.toggle').text('◀');
                    }
                    self.bus.fireEvent(
                        new mol.bus.Event('results-display-toggle', {})
                    );

                }
            );
            this.bus.addHandler(
                'show-search-hint',
                function(event) {
                    if(!self.seenHint) {
                        self.display.qtip({
                            content: {
                                text: '' +
                                    '
' +
                                        'Type a species name here and select.' +
                                    '
'
                            },
                            style: { width: {min: 400, max:500}},
                            position: {
                                my: 'top left',
                                at: 'bottom right'
                            },
                            show: {
                                event: false,
                                ready: true
                            },
                            hide: {
                                fixed: false,
                                event: 'unfocus'
                            }
                        });
                        self.seenHint=true;
                    }
                }
            );
            this.bus.addHandler(
                'close-autocomplete',
                function(event) {
                    $(self.display.searchBox).autocomplete("close");
                }
            );
            this.bus.addHandler(
                'hide-search',
                function(event) {
                    $(self.display).hide();
                }
            );
            this.bus.addHandler(
                'show-search',
                function(event) {
                    $(self.display).show();
                }
            );
            this.bus.addHandler(
                'search',
                function(event) {
                    if (event.term != undefined) {
                        if (!self.display.is(':visible')) {
                            self.bus.fireEvent(
                                new mol.bus.Event(
                                    'search-display-toggle',
                                    {visible : true}
                                )
                            );
                        }

                        self.search(event.term);

                        if (self.display.searchBox.val()=='') {
                            self.display.searchBox.val(event.term);
                        }
                    }
               }
            );

            /**
             * Clicking the go button executes a search.
             */

            this.display.click(
                function(event) {
                    $(this).qtip("hide");

                }
            )

            this.display.goButton.click(
                function(event) {
                    self.userEnteredSearch(self.display.searchBox.val());
                }
            );

            /**
             * Clicking the cancel button hides the search display and fires
             * a cancel-search event on the bus.
             */
            this.display.toggleButton.click(
                function(event) {
                    var params = {
                        visible: false
                    }, that = this;

                    if(self.display.searchDisplay.is(':visible')) {
                        self.display.searchDisplay.hide();
                        $(this).text('▶');
                        params.visible = false;
                    } else {

                        self.display.searchDisplay.show();
                        $(this).text('◀');
                        params.visible = true;
                    }

                    self.bus.fireEvent(
                        new mol.bus.Event('results-display-toggle', params));
                }
            );

            /**
             * Pressing the return button clicks the go button.
             */
            this.display.searchBox.keyup(
                function(event) {
                    if (event.keyCode === 13) {
                        $(this).autocomplete("close");
                        self.bus.fireEvent(
                            new mol.bus.Event(
                                'hide-loading-indicator',
                                {source : "autocomplete"}
                            )
                        );
                        self.userEnteredSearch($(this).val());
                    }
                }
            );
        },

        /**
         * Fires the 'add-map-control' event. The mol.map.MapEngine handles
         * this event and adds the display to the map.
         */
        fireEvents: function() {
            var params = {
                    display: this.display,
                    slot: mol.map.ControlDisplay.Slot.TOP,
                    position: google.maps.ControlPosition.TOP_LEFT
                },
                event = new mol.bus.Event('add-map-control', params);

            this.bus.fireEvent(event);
        },

        /**
         * Searches CartoDB using a term from the search box.
         * This checks the user-entered search term before
         * passing it on to search().
         */
        userEnteredSearch: function(term) {
            var self = this;

            // Trim the term. Since we later overwrite the search string,
            // this trims that as well.
            term = $.trim(term);

            // Turn off autocomplete.
            $(self.display.searchBox).autocomplete('disable');
            $(self.display.searchBox).autocomplete('close');

            // Produce an error if it's too short.
            if(term.length < 3) {
                if (term.length == 0) {
                    self.bus.fireEvent(new mol.bus.Event('clear-results'));
                } else {
                    alert('' +
                        'Please enter at least 3 characters ' +
                        'in the search box.'
                     );
                }
                return;
            }

            // Clear results, otherwise the box just stays on the screen
            // and looks ugly.
            self.bus.fireEvent(new mol.bus.Event('clear-results'));

            // Now that the term is cleaned, go through the normal search
            // procedure.
            self.search(term);
        },

        /**
         * Searches CartoDB using a term from the search box.
         *
         * @param term the search term (scientific name)
         */
        search: function(term) {
            var self = this;

            // Show loading indicator.
            self.bus.fireEvent(
                new mol.bus.Event(
                    'show-loading-indicator',
                    {source : "search-{0}".format(term)}
                )
            );

            $.ajax({
                url: 'http://mol.cartodb.com/api/v1/sql?q={0}'.format(
                    this.search_sql.format(
                        $.trim(term)
                        .replace(/ /g, ' ')
                    )
                ),
                dataType: "json",
                timeout: this.search_query_timeout,
                error: function(jqXHR, textStatus, errorThrown) {
                    console.log("Error searching for layers with term '" +
                        term + "': " + textStatus + "/" + errorThrown);

                    // Fire a 'search-results' so the synonym count is
                    // managed correctly.
                    self.bus.fireEvent(
                        new mol.bus.Event(
                            'search-results',
                            {
                                term: term,
                                response: {
                                    rows: []
                                }
                            }
                        )
                    );

                    // Reset the UI.
                    self.bus.fireEvent(
                        new mol.bus.Event(
                            'hide-loading-indicator',
                            {source : "search-{0}".format(term)}
                        )
                    );

                    $(self.display.searchBox).autocomplete('enable');
                },
                success: function (response) {
                    var results = {term:term, response:response};

                    self.bus.fireEvent(
                        new mol.bus.Event(
                            'hide-loading-indicator',
                            {source : "search-{0}".format(term)}
                        )
                    );
                    self.bus.fireEvent(
                        new mol.bus.Event(
                            'search-results',
                            results
                        )
                    );
                    $(self.display.searchBox).autocomplete('enable');
                }
            });

        }
    });

    mol.map.search.SearchDisplay = mol.mvp.View.extend({
        init: function() {
            var html = '' +
                '
' +
                '
Search
' +
                '
' +
                '       <input class="value ui-autocomplete-input" type="text" ' +
                            'placeholder="Search by species name">' +
                '       <button class="execute">Go</button>' +
                '
'+
                '   <button class="toggle">▶</button>' +
                '
';

            this._super(html);
            this.goButton = $(this).find('.execute');
            this.toggleButton = $(this).find('.toggle');
            this.searchDisplay = $(this).find('.searchDisplay');
            this.searchBox = $(this).find('.value');
        },

        clear: function() {
            this.searchBox.html('');
        }
    });
};
<|endoftext|>


<commit_before>'use strict'

import {Component}   from 'react'
import {Link}        from 'react-router'
import DocumentTitle from 'react-document-title'
import {bindActionCreators} from 'redux'
import {connect} from 'react-redux'

import {BlogActions}  from '../datastore/Blog'
import {StatsActions} from '../datastore/Stats'
import Image          from '../components/Image'

function mapStateToProps(state) {
  return {
    posts: state.blog.posts,
    stats: state.stats,
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    Object.assign({}, BlogActions, StatsActions),
    dispatch
  )
}

class HomePage extends Component {

  constructor(props) {
    super(props)

    this.state = {
      stats: this.props.stats,
      posts: this.props.posts
    }
  }

  componentWillMount() {
    if (this.props.posts.length === 0) {
      this.props.fetchPosts()
    }
    this.props.fetchStats()
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.stats !== this.props.stats) {
      const stats = nextProps.stats
      this.setState({
        stats: stats,
      })
    }

    if (nextProps.posts !== this.props.posts) {
      this.setState({
        posts: nextProps.posts,
      })
    }
  }

  render() {
    const firstThreePosts = this.state.posts.slice(0, 3)

    return (
      <DocumentTitle title="Blockstack, building the decentralized internet">

                  What will you build on the decentralized internet?

                  Blockstack is a new decentralized internet where you own your data and apps run locally without remote servers.

                  <Link to="/tutorials/hello-blockstack" role="button"
                    className="btn btn-lg btn-secondary btn-block">
                    Watch Tutorial
                  </Link>

                  <Link to="/install" className="hero-caption-text">
                    Try the browser portal   ›
                    Install
                  </Link>

                    The decentralized internet is already here

                        In production for over 2 years

                        {this.state.stats.domains} users registered

                        Used by OpenBazaar, Microsoft & more

                    The internet, evolved

                    The decentralized internet is powered by a technological breakthrough in consensus algorithms that lets you take back your safety, privacy, and property rights. Experience the internet as it was truly meant to be.

                    Blockstack gives you access to the decentralized internet within your favorite browser. Claim your identity, try out the decentralized apps, find friends in the public directory, make payments with digital currency, and connect storage providers to host and own your data.

                    <Link to="/install" role="button"
                      className="btn btn-lg btn-outline-primary btn-block">
                      Install Browser Portal
                    </Link>

                    The future of app development is decentralized

                      No infrastructure

                      Build apps without databases or server maintainance. Publish apps to the decentralized internet where they will run on user devices and live forever.

                      Get users faster

                      Make it easier than ever for users to switch with shared data protocols between apps and freedom from walled gardens, censorship, and middlemen.

                      Be paid for open source

                      New business models enable you to get paid for open source code. Utilize micropayments, blockchain protocols, and more.

                    New app possibilities to explore

                        Decentralized social networks

                        Liquid democracy

                        Peer-to-peer marketplaces

                        Open ridesharing

                        Verified file publishing

                        Encrypted health records

                    Blockstack gives you a full trustless stack

                        Identity

                        With Blockstack, users get a set of digital keys that let them own their identity.
                        From there, they can sign in to apps locally without remote servers or identity providers.

                        Storage

                        Blockstack's storage system allows users to bring their own storage providers and control their data.
                        Data is encrypted and easily shared between applications.

                        Naming

                        Blockstack's naming system (BNS), built on Blockstack's blockchain and P2P network, is completely decentralized and securely maps names to keys and routing info.

                        Payments

                        With Blockstack, you can accept simple one-time payments, enable continuous micropayments, and do much more.
                        All with a global-accepted and decentralized currency called Bitcoin.

                        Encryption

                        With Blockstack, all files and communication is end-to-end encrypted by default.
                        Blockstack handles key distribution and discovery so you don't have to.

                    Step-by-step tutorials

                    <Link to="/tutorials/hello-blockstack">
                      <Image
                        src="/images/tutorials/hello-blockstack-fastforward.gif"
                        fallbackSrc="/images/tutorials/hello-blockstack-fastforward.gif"
                        retinaSupport={false} />
                    </Link>

                    Complete the step-by-step tutorial and see how easy it is to build an app with a decentralized identity system in a few lines of code and no servers.

                    <Link to="/tutorials/hello-blockstack" role="button"
                      className="btn btn-outline-primary btn-block">
                      Hello Blockstack Tutorial
                    </Link>

                    Want to learn more about building apps on blockstack? Tutorials on building serverless apps with decentralized storage are coming soon.

                    News

                  { firstThreePosts.map((post, index) => {
                    return (

                        { post.urlSlug && post.title ?
                        <Link to={'/blog/' + post.urlSlug}>

{ post.title }

                        </Link>
                        : null }
                        { post.preview ?

                        : null }

                          { post.creator ?
                          {post.creator.name} |
                          : null }
                          { post.datetime && post.date ?
                          <time className="post-date" dateTime={post.datetime}>
                            {post.date}
                          </time>
                          : null }

                    )
                  }) }

                    <Link to="/newsletter" role="button"
                      className="btn btn-lg btn-outline-primary btn-block">
                      Get Updates
                    </Link>

      </DocumentTitle>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(HomePage)<commit_msg>update home page copy<commit_after>'use strict'

import {Component}   from 'react'
import {Link}        from 'react-router'
import DocumentTitle from 'react-document-title'
import {bindActionCreators} from 'redux'
import {connect} from 'react-redux'

import {BlogActions}  from '../datastore/Blog'
import {StatsActions} from '../datastore/Stats'
import Image          from '../components/Image'

function mapStateToProps(state) {
  return {
    posts: state.blog.posts,
    stats: state.stats,
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    Object.assign({}, BlogActions, StatsActions),
    dispatch
  )
}

class HomePage extends Component {

  constructor(props) {
    super(props)

    this.state = {
      stats: this.props.stats,
      posts: this.props.posts
    }
  }

  componentWillMount() {
    if (this.props.posts.length === 0) {
      this.props.fetchPosts()
    }
    this.props.fetchStats()
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.stats !== this.props.stats) {
      const stats = nextProps.stats
      this.setState({
        stats: stats,
      })
    }

    if (nextProps.posts !== this.props.posts) {
      this.setState({
        posts: nextProps.posts,
      })
    }
  }

  render() {
    const firstThreePosts = this.state.posts.slice(0, 3)

    return (
      <DocumentTitle title="Blockstack, building the decentralized internet">

                  What will you build on the decentralized internet?

                  Blockstack is a new decentralized internet where you own your data and apps run locally without remote servers.

                  <Link to="/tutorials/hello-blockstack" role="button"
                    className="btn btn-lg btn-secondary btn-block">
                    Watch Tutorial
                  </Link>

                  <Link to="/install" className="hero-caption-text">
                    Try the browser portal   ›
                    Install
                  </Link>

                    The decentralized internet is already here

                        In production for over 2 years

                        {this.state.stats.domains} users registered

                        Used by OpenBazaar, Microsoft & more

                    The internet, evolved

                    The decentralized internet is powered by a technological breakthrough in consensus algorithms that lets you take back your safety, privacy, and property rights. Experience the internet as it was truly meant to be.

                    Blockstack gives you access to the decentralized internet within your favorite browser. Claim your identity, try out the decentralized apps, find friends in the public directory, make payments with digital currency, and connect storage providers to host and own your data.

                    <Link to="/install" role="button"
                      className="btn btn-lg btn-outline-primary btn-block">
                      Install Browser Portal
                    </Link>

                    The future of app development is decentralized

                      No infrastructure

                      Build apps without databases or server maintainance. Publish apps to the decentralized internet where they will run on user devices and live forever.

                      Get users faster

                      Make it easier than ever for users to switch with shared data protocols between apps and freedom from walled gardens, censorship, and middlemen.

                      Be paid for open source

                      New business models enable you to get paid for open source code. Utilize micropayments, blockchain protocols, and more.

                    New app possibilities to explore

                        Decentralized social networks

                        Liquid democracy

                        Peer-to-peer marketplaces

                        Open ridesharing

                        Verified file publishing

                        Encrypted health records

                    Blockstack gives you a full trustless stack

                        Identity & Auth

                        With Blockstack, users get a set of digital keys that let them own their identity.
                        From there, they can sign in to apps locally without remote servers or identity providers.

                        Storage

                        Blockstack's storage system allows users to bring their own storage providers and control their data.
                        Data is encrypted and easily shared between applications.

                        Naming

                        Blockstack's naming system (BNS), built on Blockstack's blockchain and P2P network, is completely decentralized and securely maps names to keys and routing info.

                        Payments

                        With Blockstack, you can accept simple one-time payments, enable continuous micropayments, and do much more.
                        All with a global-accepted and decentralized currency called Bitcoin.

                        Encryption

                        With Blockstack, all files and communication is end-to-end encrypted by default.
                        Blockstack handles key distribution and discovery so you don't have to.

                    Step-by-step tutorials

                    <Link to="/tutorials/hello-blockstack">
                      <Image
                        src="/images/tutorials/hello-blockstack-fastforward.gif"
                        fallbackSrc="/images/tutorials/hello-blockstack-fastforward.gif"
                        retinaSupport={false} />
                    </Link>

                    Complete the step-by-step tutorial and see how easy it is to build an app with a decentralized identity system in a few lines of code and no servers.

                    <Link to="/tutorials/hello-blockstack" role="button"
                      className="btn btn-outline-primary btn-block">
                      Hello Blockstack Tutorial
                    </Link>

                    Want to learn more about building apps on blockstack? Tutorials on building serverless apps with decentralized storage are coming soon.

                    News

                  { firstThreePosts.map((post, index) => {
                    return (

                        { post.urlSlug && post.title ?
                        <Link to={'/blog/' + post.urlSlug}>

{ post.title }

                        </Link>
                        : null }
                        { post.preview ?

                        : null }

                          { post.creator ?
                          {post.creator.name} |
                          : null }
                          { post.datetime && post.date ?
                          <time className="post-date" dateTime={post.datetime}>
                            {post.date}
                          </time>
                          : null }

                    )
                  }) }

                    <Link to="/newsletter" role="button"
                      className="btn btn-lg btn-outline-primary btn-block">
                      Get Updates
                    </Link>

      </DocumentTitle>
    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(HomePage)<|endoftext|>