├── .ncurc
├── next.config.js
├── .gitignore
├── static
└── images
│ ├── logo.png
│ ├── favicon.ico
│ ├── spacer.png
│ ├── clean-hr.png
│ ├── background.jpeg
│ ├── video.svg
│ ├── play-circle.svg
│ └── spectrum.svg
├── utils
├── history.js
├── slack.js
├── pointFreePromise.js
├── __tests__
│ ├── github.test.js
│ └── utils.test.js
├── jobs.js
├── github.js
├── mailchimp.js
├── getAsync.js
├── index.js
├── auth.js
└── api.js
├── .nsprc
├── components
├── __tests__
│ ├── __snapshots__
│ │ ├── Title.test.js.snap
│ │ ├── Pulse.test.js.snap
│ │ ├── CleanHr.test.js.snap
│ │ ├── FadeIn.test.js.snap
│ │ ├── Hideable.test.js.snap
│ │ ├── BounceInUp.test.js.snap
│ │ ├── SlideInUp.test.js.snap
│ │ ├── Paragraph.test.js.snap
│ │ ├── VerticalyCentered.test.js.snap
│ │ ├── Play.test.js.snap
│ │ ├── Subtitle.test.js.snap
│ │ ├── Smartphone.test.js.snap
│ │ ├── GithubRibbon.test.js.snap
│ │ ├── ViewIcon.test.js.snap
│ │ ├── Love.test.js.snap
│ │ ├── Comment.test.js.snap
│ │ ├── Notice.test.js.snap
│ │ ├── Gif.test.js.snap
│ │ └── MailchimpForm.test.js.snap
│ ├── Love.test.js
│ ├── Pulse.test.js
│ ├── Title.test.js
│ ├── FadeIn.test.js
│ ├── Notice.test.js
│ ├── CleanHr.test.js
│ ├── Comment.test.js
│ ├── Footer.test.js
│ ├── Hideable.test.js
│ ├── ViewIcon.test.js
│ ├── Paragraph.test.js
│ ├── SlideInUp.test.js
│ ├── BounceInUp.test.js
│ ├── GithubRibbon.test.js
│ ├── VerticalyCentered.test.js
│ ├── Play.test.js
│ ├── Subtitle.test.js
│ ├── Smartphone.test.js
│ ├── Wrapper.test.js
│ ├── Gif.test.js
│ └── MailchimpForm.test.js
├── VerticalyCentered.js
├── Paragraph.js
├── Pulse.js
├── Gif
│ ├── GifContainer.js
│ ├── Play.js
│ ├── Button.js
│ ├── Smartphone.js
│ └── index.js
├── BounceInUp.js
├── Container.js
├── FadeIn.js
├── Hideable.js
├── SlideInUp.js
├── CleanHr.js
├── Title.js
├── Subtitle.js
├── JobAd.js
├── ViewIcon.js
├── Comment.js
├── Speech.js
├── Octicon.js
├── Love.js
├── NumberList.js
├── Icon.js
├── GithubRibbon.js
├── Notice.js
├── Wrapper.js
├── MailchimpForm.js
├── SocialBar.js
└── Footer.js
├── newrelic.js
├── .babelrc
├── pages
├── sign-out.js
├── developers.js
├── sign-in.js
├── signed-in.js
├── player.js
├── about.js
├── _document.js
├── index.js
├── upload.js
└── appdetail.js
├── server
├── sitemap.xml
├── es
│ ├── __tests__
│ │ ├── user.test.js
│ │ ├── gif.test.js
│ │ └── es.test.js
│ ├── mappings
│ │ ├── gallery-user_user.json
│ │ └── gallery_gif.json
│ ├── user.js
│ ├── gif.js
│ ├── endpoints.js
│ └── index.js
├── robots.txt
└── index.js
├── env-config.js
├── templates
└── Component.test.txt
├── .eslintrc
├── testSetup.js
├── scripts
├── get_doc.js
├── init_es_index_type.js
├── delete_doc.js
└── create_gif.js
├── .github
└── FUNDING.yml
├── reset.css.js
├── plopfile.js
├── circle.yml
├── LICENCE
├── hocs
└── defaultPage.js
├── README.md
└── package.json
/.ncurc:
--------------------------------------------------------------------------------
1 | {
2 | "reject": "chalk"
3 | }
4 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | xPoweredBy: false
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | tmp
4 | .vscode
5 | *.log
6 | newrelic_agent.log
7 | .env
8 | coverage/
--------------------------------------------------------------------------------
/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeGallery/reactnative-gallery-web/HEAD/static/images/logo.png
--------------------------------------------------------------------------------
/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeGallery/reactnative-gallery-web/HEAD/static/images/favicon.ico
--------------------------------------------------------------------------------
/static/images/spacer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeGallery/reactnative-gallery-web/HEAD/static/images/spacer.png
--------------------------------------------------------------------------------
/static/images/clean-hr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeGallery/reactnative-gallery-web/HEAD/static/images/clean-hr.png
--------------------------------------------------------------------------------
/static/images/background.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeGallery/reactnative-gallery-web/HEAD/static/images/background.jpeg
--------------------------------------------------------------------------------
/utils/history.js:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 |
3 | export default {
4 | replace() {
5 | Router.push('/')
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.nsprc:
--------------------------------------------------------------------------------
1 | {
2 | "exceptions": [
3 | "https://nodesecurity.io/advisories/566",
4 | "https://nodesecurity.io/advisories/577"
5 | ]
6 | }
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Title.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`
toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Pulse.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/CleanHr.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/FadeIn.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/VerticalyCentered.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const VerticalyCentered = styled.div`
4 | margin-top: 50vh;
5 | transform: translateY(-50%);
6 | `
7 |
8 | export default VerticalyCentered
9 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Hideable.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/BounceInUp.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/SlideInUp.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Paragraph.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/VerticalyCentered.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/utils/slack.js:
--------------------------------------------------------------------------------
1 | /* eslint import/prefer-default-export: 0 */
2 | require('isomorphic-fetch')
3 |
4 | export const getSlackDataAsync = async () => {
5 | const result = await fetch(`${process.env.SLACK_IN}/data`)
6 | return result.json()
7 | }
8 |
--------------------------------------------------------------------------------
/utils/pointFreePromise.js:
--------------------------------------------------------------------------------
1 | const { curry } = require('ramda')
2 |
3 | const then = curry((f, thenable) => thenable.then(f))
4 |
5 | const catchP = curry((f, promise) => promise.catch(f))
6 |
7 | module.exports = {
8 | then,
9 | catchP
10 | }
11 |
--------------------------------------------------------------------------------
/components/Paragraph.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Subtitle = styled.p`
4 | font-size: 17px;
5 | line-height: 26px;
6 | @media (max-width: 769px) {
7 | font-size: 14px;
8 | }
9 | `
10 |
11 | export default Subtitle
12 |
--------------------------------------------------------------------------------
/newrelic.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const { NEWRELIC_LICENSE_KEY } = process.env
4 |
5 | exports.config = {
6 | app_name: ['reactnative.gallery'],
7 | license_key: NEWRELIC_LICENSE_KEY,
8 | logging: {
9 | level: 'info',
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "next/babel"],
3 | "plugins": [
4 | [
5 | "styled-components",
6 | { "ssr": true, "displayName": true, "preprocess": false }
7 | ],
8 | ["transform-define-file", { "file": "./env-config.js" }]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/components/Pulse.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components'
2 | import { tada } from 'react-animations'
3 |
4 | const pulser = keyframes`${tada}`
5 |
6 | const Pulse = styled.div`
7 | animation: 1.5s ${pulser};
8 | `
9 |
10 | export default Pulse
11 |
--------------------------------------------------------------------------------
/components/Gif/GifContainer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const GifContainer = styled.div`
4 | position: relative;
5 | padding: 0px;
6 | font-size: 0px;
7 | max-width: 250px;
8 | margin: 0px auto;
9 | `
10 |
11 | export default GifContainer
12 |
--------------------------------------------------------------------------------
/components/BounceInUp.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components'
2 | import { bounceInUp } from 'react-animations'
3 |
4 | const bouncer = keyframes`${bounceInUp}`
5 |
6 | const BounceInUp = styled.div`
7 | animation: 2s ${bouncer};
8 | `
9 |
10 | export default BounceInUp
11 |
--------------------------------------------------------------------------------
/pages/sign-out.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { unsetToken, logout } from '../utils/auth'
4 |
5 | export default class SignOff extends React.Component {
6 | componentDidMount() {
7 | unsetToken()
8 | logout()
9 | }
10 | render() {
11 | return null
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://reactnative.gallery/
4 |
5 |
6 | https://reactnative.gallery/upload/
7 |
8 |
9 | https://reactnative.gallery/about/
10 |
11 |
--------------------------------------------------------------------------------
/utils/__tests__/github.test.js:
--------------------------------------------------------------------------------
1 | import { getFullNameFormUrl } from '../github'
2 | import pkg from '../../package.json'
3 |
4 | it('should get fullname from github url', () => {
5 | const fullname = 'ReactNativeGallery/reactnative-gallery-web'
6 | expect(getFullNameFormUrl(pkg.repository.url)).toBe(fullname)
7 | })
8 |
--------------------------------------------------------------------------------
/components/Container.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Container = styled.div`
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | flex-direction: column;
8 | padding: 15px;
9 | color: #fff;
10 | max-width: 860px;
11 | `
12 |
13 | export default Container
14 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Play.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
9 | exports[` toMatchSnapshot 2 1`] = `
10 |
13 | `;
14 |
--------------------------------------------------------------------------------
/components/FadeIn.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components'
2 | import { fadeIn } from 'react-animations'
3 |
4 | const fader = keyframes`${fadeIn}`
5 |
6 | const FadeIn = styled.div`
7 | animation: ${props => props.timer || 1}s ${props => props.delay || 0}ms
8 | ${fader};
9 | `
10 |
11 | export default FadeIn
12 |
--------------------------------------------------------------------------------
/static/images/video.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Hideable.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Hideable = styled.span`
4 | @media (max-width: 481px) {
5 | display: ${props => (props.xs ? 'none' : 'block')};
6 | }
7 | @media (min-width: 481px) {
8 | display: ${props => (props.md ? 'none' : 'block')};
9 | }
10 | `
11 |
12 | export default Hideable
13 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Subtitle.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
9 | exports[` toMatchSnapshot 2 1`] = `
10 |
13 | `;
14 |
--------------------------------------------------------------------------------
/components/SlideInUp.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components'
2 | import { slideInUp } from 'react-animations'
3 |
4 | const slide = keyframes`${slideInUp}`
5 |
6 | const SlideInUp = styled.div`
7 | animation: ${props => props.timer || 1}s ${props => props.delay || 0}ms
8 | ${slide};
9 | `
10 |
11 | export default SlideInUp
12 |
--------------------------------------------------------------------------------
/server/es/__tests__/user.test.js:
--------------------------------------------------------------------------------
1 | const user = require('../user')
2 |
3 | it('user.indexUserAsync', async () => {
4 | expect(await user.indexUserAsync({ id: 'test', key: 'value' })).toEqual({
5 | body: [
6 | { index: { _id: 'test', _index: 'gallery-user', _type: 'user' } },
7 | { id: 'test', key: 'value' }
8 | ]
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/components/CleanHr.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const CleanHr = styled.div`
4 | z-index: 0;
5 | width: 100%;
6 | margin: 0 auto;
7 | height: 8px;
8 | max-width: 579.5px;
9 | margin-top: 28px;
10 | margin-bottom: 28px;
11 | background-image: url('/static/images/clean-hr.png');
12 | `
13 |
14 | export default CleanHr
15 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Smartphone.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 | `;
8 |
9 | exports[` toMatchSnapshot 2 1`] = `
10 |
13 | `;
14 |
--------------------------------------------------------------------------------
/static/images/play-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/__tests__/Love.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Love from '../Love'
4 |
5 | it('Love can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/Title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Title = styled.h1`
4 | text-align: center;
5 | color: #fff;
6 | @media (min-width: 481px) {
7 | font-size: 35px;
8 | }
9 | @media (min-width: 769px) {
10 | font-size: 45px;
11 | }
12 | @media (max-width: 769px) {
13 | display: ${props => (props.hidexs ? 'none' : 'block')};
14 | }
15 | `
16 |
17 | export default Title
18 |
--------------------------------------------------------------------------------
/components/__tests__/Pulse.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Pulse from '../Pulse'
4 |
5 | it('Pulse can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/Title.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Title from '../Title'
4 |
5 | it('Title can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/FadeIn.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import FadeIn from '../FadeIn'
4 |
5 | it('FadeIn can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/Notice.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Notice from '../Notice'
4 |
5 | it('Notice can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/CleanHr.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import CleanHr from '../CleanHr'
4 |
5 | it('CleanHr can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/Comment.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Comment from '../Comment'
4 |
5 | it('Comment can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/Footer.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Footer from '../Footer'
4 |
5 | it('Footer can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | // it(' toMatchSnapshot', () => {
11 | // const tree = renderer.create().toJSON()
12 | // expect(tree).toMatchSnapshot()
13 | // })
14 |
--------------------------------------------------------------------------------
/components/__tests__/Hideable.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Hideable from '../Hideable'
4 |
5 | it('Hideable can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/ViewIcon.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import ViewIcon from '../ViewIcon'
4 |
5 | it('ViewIcon can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/Paragraph.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Paragraph from '../Paragraph'
4 |
5 | it('Paragraph can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/SlideInUp.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import SlideInUp from '../SlideInUp'
4 |
5 | it('SlideInUp can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/BounceInUp.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import BounceInUp from '../BounceInUp'
4 |
5 | it('BounceInUp can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/Gif/Play.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Play = styled.div`
4 | @media (max-width: 769px) {
5 | position: absolute;
6 | z-index: 100;
7 | margin: 0px auto;
8 | width: 100%;
9 | height: 100%;
10 | max-width: 275px;
11 | opacity: 0.5;
12 | content: url(/static/images/play-circle.svg);
13 | display: ${props => (props.show ? 'block' : 'none')};
14 | }
15 | `
16 |
17 | export default Play
18 |
--------------------------------------------------------------------------------
/components/__tests__/GithubRibbon.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import GithubRibbon from '../GithubRibbon'
4 |
5 | it('GithubRibbon can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/Subtitle.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Subtitle = styled.h3`
4 | position: relative;
5 | z-index: 100;
6 | text-align: center;
7 | color: #fff;
8 | @media (min-width: 481px) {
9 | font-size: 20px;
10 | }
11 | @media (min-width: 769px) {
12 | font-size: 25px;
13 | }
14 | @media (max-width: 769px) {
15 | display: ${props => (props.hidexs ? 'none' : 'block')};
16 | }
17 | `
18 |
19 | export default Subtitle
20 |
--------------------------------------------------------------------------------
/components/JobAd.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 |
5 | const JobAdd = styled.p`
6 | font-size: 17px;
7 | line-height: 26px;
8 | @media (max-width: 769px) {
9 | font-size: 14px;
10 | }
11 | `
12 |
13 | const JobAddContainer = ({ title }) => {title}
14 |
15 | JobAddContainer.propTypes = {
16 | title: PropTypes.string.isRequired
17 | }
18 |
19 | export default JobAddContainer
20 |
--------------------------------------------------------------------------------
/components/ViewIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Eye from 'react-feather/dist/icons/eye'
4 | import { Icon, renderSmallIcon } from './Icon'
5 |
6 | const Comment = ({ number }) => (
7 |
8 | {renderSmallIcon(Eye)}
9 | {number}
10 |
11 | )
12 |
13 | Comment.propTypes = {
14 | number: PropTypes.number
15 | }
16 |
17 | Comment.defaultProps = {
18 | number: 0
19 | }
20 | export default Comment
21 |
--------------------------------------------------------------------------------
/components/__tests__/VerticalyCentered.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import VerticalyCentered from '../VerticalyCentered'
4 |
5 | it('VerticalyCentered can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/env-config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const vars = [
4 | 'NODE_ENV',
5 | 'AUTH_ID',
6 | 'AUTH_DOMAIN',
7 | 'MAILCHIMP_ACTION',
8 | 'SLACK_IN',
9 | 'MAILCHIMP_MEMBER_COUNT_DEFAULT',
10 | 'BASE_SOURCE_GIF_GIANT',
11 | 'BASE_SOURCE_GIF_THUMBS',
12 | 'BASE_GIF_API',
13 | 'BASE_GIF_UPLOAD'
14 | ]
15 |
16 | const env = vars.reduce(
17 | (prev, curr) =>
18 | Object.assign(prev, { [`process.env.${curr}`]: process.env[curr] }),
19 | {}
20 | )
21 |
22 | module.exports = env
23 |
--------------------------------------------------------------------------------
/templates/Component.test.txt:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import {{ properCase name }} from '../{{ properCase name }}'
4 |
5 | it('{{ properCase name }} can be created', () => {
6 | const comp = renderer.create(<{{ properCase name }} />)
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it('<{{ properCase name }} /> toMatchSnapshot', () => {
11 | const tree = renderer.create(<{{ properCase name }} />).toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/GithubRibbon.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
9 |
14 |
15 | `;
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "rules": {
5 | "comma-dangle": 0,
6 | "semi": ["error", "never"],
7 | "react/jsx-filename-extension": 0,
8 | "max-len": ["error", { "code": 80 }],
9 | "camelcase": 0,
10 | "no-extra-boolean-cast": 0
11 | },
12 | "globals": {
13 | "window": false,
14 | "it": false,
15 | "expect": false,
16 | "jest": false,
17 | "fetch": false,
18 | "history": false,
19 | "localStorage": false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components/Comment.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import MessageCircle from 'react-feather/dist/icons/message-circle'
4 | import { Icon, renderSmallIcon } from './Icon'
5 |
6 | const Comment = ({ number }) => (
7 |
8 | {renderSmallIcon(MessageCircle)}
9 | {number}
10 |
11 | )
12 |
13 | Comment.propTypes = {
14 | number: PropTypes.number
15 | }
16 |
17 | Comment.defaultProps = {
18 | number: 0
19 | }
20 | export default Comment
21 |
--------------------------------------------------------------------------------
/components/Speech.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Speech = styled.div`
4 | margin: 30px;
5 | background-color: rgba(255, 255, 255, 0.72);
6 | padding: 65px 80px;
7 | border-radius: 15px;
8 | border-width: 1;
9 | border-color: #BBB8A9,
10 | border-style: solid;
11 | color: #444;
12 | box-shadow: 1px 1px 3px grey;
13 | text-shadow: 0.2px 0.2px lightgrey;
14 | @media (max-width: 769px) {
15 | padding: 15px;
16 | margin: 15px;
17 | }
18 | `
19 |
20 | export default Speech
21 |
--------------------------------------------------------------------------------
/pages/developers.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Users from 'react-feather/dist/icons/users'
3 | import Paragraph from '../components/Paragraph'
4 | import Container from '../components/Container'
5 | import Speech from '../components/Speech'
6 | import defaultPage from '../hocs/defaultPage'
7 |
8 | const About = () => (
9 |
10 |
11 |
12 | Coming soon!
13 |
14 |
15 | )
16 |
17 | export default defaultPage(About)
18 |
--------------------------------------------------------------------------------
/pages/sign-in.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import querystring from 'querystring'
3 | import VerticalyCentered from '../components/VerticalyCentered'
4 |
5 | import { show } from '../utils/auth'
6 |
7 | const CONTAINER_ID = 'put-lock-here'
8 |
9 | class SignIn extends React.Component {
10 | componentDidMount() {
11 | show(CONTAINER_ID, querystring.parse(window.location.search.substr(1)))
12 | }
13 | render() {
14 | return
15 | }
16 | }
17 |
18 | export default SignIn
19 |
--------------------------------------------------------------------------------
/utils/jobs.js:
--------------------------------------------------------------------------------
1 | const { getAsync } = require('./getAsync')
2 |
3 | const defaultValue = {
4 | data: [
5 | {
6 | title: 'Find a Job or post a Job!',
7 | siteUrl: 'https://jobs.reactnative.gallery'
8 | }
9 | ]
10 | }
11 |
12 | async function getJobsAsync() {
13 | const { data } = await getAsync(
14 | { url: 'https://jobs.reactnative.gallery/api/jobs' },
15 | defaultValue
16 | )
17 | return data.length ? data : defaultValue.data
18 | }
19 |
20 | module.exports = {
21 | getJobsAsync
22 | }
23 |
--------------------------------------------------------------------------------
/components/Gif/Button.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const ButtonContainer = styled.div`
4 | position: absolute;
5 | width: 100%;
6 | height: 45px;
7 | right: 0;
8 | padding-top: 7px;
9 | `
10 |
11 | export const Button = styled.div`
12 | margin: 0px auto;
13 | border: ${props => (props.hover ? 'solid 3px #eee' : 'solid 3px #707070')};
14 | width: 45px;
15 | height: 45px;
16 | border-radius: 90px;
17 | cursor: pointer;
18 | background: ${props => (props.cliked ? '#eee' : 'transparent')};
19 | `
20 |
--------------------------------------------------------------------------------
/server/es/mappings/gallery-user_user.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": {
3 | "id": {
4 | "type": "keyword"
5 | },
6 | "email": {
7 | "type": "keyword"
8 | },
9 | "emailVerified": {
10 | "type": "boolean"
11 | },
12 | "published": {
13 | "type": "boolean"
14 | },
15 | "picture": {
16 | "type": "keyword"
17 | },
18 | "name": {
19 | "type": "text"
20 | },
21 | "createdAt": {
22 | "type": "date"
23 | },
24 | "updatedAt": {
25 | "type": "date"
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/components/__tests__/Play.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Play from '../Gif/Play'
4 |
5 | it('Play can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
15 | it(' toMatchSnapshot 2', () => {
16 | const tree = renderer.create().toJSON()
17 | expect(tree).toMatchSnapshot()
18 | })
19 |
--------------------------------------------------------------------------------
/testSetup.js:
--------------------------------------------------------------------------------
1 | jest.mock('elasticsearch', () => ({
2 | Client: () => ({
3 | ping: () => Promise.resolve(true),
4 | bulk: bulkables => Promise.resolve(bulkables),
5 | msearch: searches => Promise.resolve(searches),
6 | get: ({ index, type, id }) => Promise.resolve({ index, type, id }),
7 | indices: {
8 | exists: ({ index }) => Promise.resolve(index !== 'gallery'),
9 | existsType: ({ type }) => Promise.resolve(type !== 'gif'),
10 | create: () => Promise.resolve({}),
11 | putMapping: () => Promise.resolve({})
12 | }
13 | })
14 | }))
15 |
--------------------------------------------------------------------------------
/scripts/get_doc.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const { getByIdAsync } = require('../server/es')
3 | const {
4 | invariantsUndef, logError, logInfo, jsonToString
5 | } = require('../utils')
6 |
7 | // eslint-disable-next-line
8 | const [_, __, index, type, id] = process.argv;
9 | (async () => {
10 | try {
11 | invariantsUndef({ index, type, id })
12 | const response = await getByIdAsync(index, type, id)
13 | logInfo(`index=${index} type=${type} id=${id} \n${jsonToString(response)}`)
14 | } catch (error) {
15 | logError(error)
16 | process.exit(1)
17 | }
18 | })()
19 |
--------------------------------------------------------------------------------
/components/Octicon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Github from 'react-feather/dist/icons/github'
4 | import { Icon, renderSmallIcon } from './Icon'
5 |
6 | const Octicon = ({ number, link }) => (
7 | window.open(link)}>
8 | {renderSmallIcon(Github)}
9 | {number}
10 |
11 | )
12 |
13 | Octicon.propTypes = {
14 | number: PropTypes.number,
15 | link: PropTypes.string.isRequired
16 | }
17 |
18 | Octicon.defaultProps = {
19 | number: 0
20 | }
21 | export default Octicon
22 |
--------------------------------------------------------------------------------
/scripts/init_es_index_type.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const { initIndexTypeAsync } = require('../server/es')
3 | const {
4 | invariantsUndef, logError, logInfo, jsonToString
5 | } = require('../utils')
6 |
7 | // eslint-disable-next-line
8 | const [_, __, index, type] = process.argv;
9 | (async () => {
10 | try {
11 | invariantsUndef({ index, type })
12 | const result = await initIndexTypeAsync(index, type)
13 | logInfo(`init ${index}, ${type} succeeded \n\n${jsonToString(result)}`)
14 | } catch (error) {
15 | logError(error)
16 | process.exit(1)
17 | }
18 | })()
19 |
--------------------------------------------------------------------------------
/scripts/delete_doc.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const { deleteByIdAsync } = require('../server/es')
3 | const {
4 | invariantsUndef, logError, logInfo, jsonToString
5 | } = require('../utils')
6 |
7 | // eslint-disable-next-line
8 | const [_, __, index, type, id] = process.argv;
9 | (async () => {
10 | try {
11 | invariantsUndef({ index, type, id })
12 | const response = await deleteByIdAsync(index, type, id)
13 | logInfo(`index=${index} type=${type} id=${id} \n ${jsonToString(response)}`)
14 | } catch (error) {
15 | logError(error)
16 | process.exit(1)
17 | }
18 | })()
19 |
--------------------------------------------------------------------------------
/components/__tests__/Subtitle.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Subtitle from '../Subtitle'
4 |
5 | it('Subtitle can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
15 | it(' toMatchSnapshot 2', () => {
16 | const tree = renderer.create().toJSON()
17 | expect(tree).toMatchSnapshot()
18 | })
19 |
--------------------------------------------------------------------------------
/components/__tests__/Smartphone.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Smartphone from '../Gif/Smartphone'
4 |
5 | it('Smartphone can be created', () => {
6 | const comp = renderer.create()
7 | expect(comp).toBeDefined()
8 | })
9 |
10 | it(' toMatchSnapshot', () => {
11 | const tree = renderer.create().toJSON()
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
15 | it(' toMatchSnapshot 2', () => {
16 | const tree = renderer.create().toJSON()
17 | expect(tree).toMatchSnapshot()
18 | })
19 |
--------------------------------------------------------------------------------
/server/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: BLEXBot
2 | User-Agent: AhrefsBot
3 | User-Agent: ia_archiver
4 | User-Agent: ia_archiver/1.6
5 | User-Agent: Mozilla/4.0 (compatible; BullsEye; Windows 95)
6 | User-Agent: MS Search 4.0 Robot
7 | User-Agent: MS Search 5.0 Robot
8 | User-Agent: Naverbot
9 | User-Agent: SemrushBot
10 | User-Agent: SeznamBot/3.0
11 | User-Agent: Sogou web spider
12 | User-Agent: Xenu Link Sleuth/1.3.8
13 | User-Agent: Xenu's
14 | User-Agent: Xenu's Link Sleuth 1.1c
15 | User-Agent: yandex
16 | User-Agent: YandexBot
17 | Disallow: /
18 |
19 | User-Agent: *
20 | Disallow: /auth/
21 |
22 |
23 |
24 | Sitemap: https://reactnative.gallery/sitemap.xml
--------------------------------------------------------------------------------
/components/Love.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Heart from 'react-feather/dist/icons/heart'
4 | import { Icon, renderSmallIcon } from './Icon'
5 |
6 | const Love = ({ number, onClick, checked }) => (
7 |
8 | {renderSmallIcon(Heart, { fill: checked ? '#fff' : 'none' })}
9 | {number}
10 |
11 | )
12 |
13 | Love.propTypes = {
14 | number: PropTypes.number,
15 | onClick: PropTypes.func,
16 | checked: PropTypes.bool
17 | }
18 |
19 | Love.defaultProps = {
20 | number: 0,
21 | checked: false,
22 | onClick: () => {}
23 | }
24 | export default Love
25 |
--------------------------------------------------------------------------------
/utils/github.js:
--------------------------------------------------------------------------------
1 | require('isomorphic-fetch')
2 | const invariant = require('invariant')
3 | const { getAsync } = require('./getAsync')
4 |
5 | const BASE_API = 'https://api.github.com/'
6 |
7 | const defaultResult = {
8 | stargazers_count: 0
9 | }
10 |
11 | export const getStargazersCountAsync = async (fullName) => {
12 | const { stargazers_count } = await getAsync(
13 | { url: `${BASE_API}repos/${fullName}` },
14 | defaultResult
15 | )
16 | return stargazers_count
17 | }
18 |
19 | export const getFullNameFormUrl = githubUrl =>
20 | invariant(githubUrl, 'githubUrl is undefined') ||
21 | githubUrl
22 | .split('/')
23 | .slice(3)
24 | .join('/')
25 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: xcarpentier
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/static/images/spectrum.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/create_gif.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { createGifAsync } = require('../server/es/gif')
4 | const {
5 | now, invariantsUndef, logError, logInfo, jsonToString
6 | } = require('../utils')
7 |
8 | // eslint-disable-next-line
9 | const [_, __, id] = process.argv;
10 | (async () => {
11 | try {
12 | invariantsUndef({ id })
13 | const response = await createGifAsync({
14 | id,
15 | uploadedAt: now(),
16 | like: 0,
17 | numberOfView: 0,
18 | published: true,
19 | createdAt: now(),
20 | updatedAt: now()
21 | })
22 | logInfo(`Create gif with id=${id} succeeded \n\n${jsonToString(response)}`)
23 | } catch (error) {
24 | logError(error)
25 | process.exit(1)
26 | }
27 | })()
28 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/ViewIcon.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 |
27 |
30 | 0
31 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/reset.css.js:
--------------------------------------------------------------------------------
1 | /* eslint
2 | no-unused-expressions: 0,
3 | import/no-webpack-loader-syntax: 0,
4 | import/no-unresolved: 0
5 | */
6 |
7 | import { injectGlobal } from 'styled-components'
8 |
9 | import normalize from '!!raw-loader!normalize.css/normalize.css'
10 |
11 | injectGlobal`${normalize}`
12 |
13 | injectGlobal`
14 | body {
15 | margin: 0;
16 | background: #be93c5;
17 | background: -webkit-linear-gradient(to right, #7bc6cc, #be93c5);
18 | background: linear-gradient(to right, #7bc6cc, #be93c5);
19 | -webkit-overflow-scrolling: touch;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | font-family: 'Open Sans', sans-serif;
23 | margin-bottom: 100px;
24 | height: auto;
25 | }`
26 |
--------------------------------------------------------------------------------
/plopfile.js:
--------------------------------------------------------------------------------
1 | function config(plop) {
2 | plop.setGenerator('Component test file', {
3 | description: 'Generate a test file for a component',
4 | prompts: [
5 | {
6 | type: 'input',
7 | name: 'name',
8 | message: "What is the component's name?",
9 | validate(value) {
10 | if (/.+/.test(value)) {
11 | return true
12 | }
13 | return 'component name is required'
14 | }
15 | }
16 | ],
17 | actions: [
18 | {
19 | type: 'add',
20 | path: 'components/__tests__/{{properCase name}}.test.js',
21 | templateFile: 'templates/Component.test.txt',
22 | abortOnFail: true
23 | }
24 | ]
25 | })
26 | }
27 |
28 | module.exports = config
29 |
--------------------------------------------------------------------------------
/components/Gif/Smartphone.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Smartphone = styled.div`
4 | position: relative;
5 | padding: 65px 12px;
6 | background: rgba(49, 49, 49, 0.8);
7 | border-radius: 38px;
8 | box-shadow: inset 0 0 3px 0 rgb(79, 86, 95, 0.2);
9 | border: solid 1px rgb(79, 86, 95, 0.2);
10 | max-width: 275px;
11 | min-width: ${({ minWidth }) => (minWidth && `${minWidth}px`) || '194px'};
12 | min-height: 250px;
13 | margin: auto;
14 | cursor: ${({ cursorPointer }) =>
15 | (cursorPointer ? 'pointer' : 'url(/static/images/play-circle.svg), auto;')};
16 | transform: ${({ rotate }) => (rotate ? 'rotate(-0.25turn)' : 'none')};
17 | margin-top: ${({ rotate }) => (rotate ? '-100px' : 'auto')};
18 | `
19 |
20 | export default Smartphone
21 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Love.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
8 |
23 |
26 | 0
27 |
28 |
29 | `;
30 |
--------------------------------------------------------------------------------
/pages/signed-in.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 | import {
4 | setTokenAsync,
5 | checkSecret,
6 | extractInfoFromHash,
7 | saveUserAsync
8 | } from '../utils/auth'
9 |
10 | class SignedIn extends React.Component {
11 | async componentDidMount() {
12 | const { token, secret, next } = extractInfoFromHash()
13 | if (!checkSecret(secret) || !token) {
14 | // eslint-disable-next-line
15 | console.error('Something happened with the Sign In request')
16 | }
17 | const user = await setTokenAsync(token)
18 | if (user) {
19 | await saveUserAsync(user)
20 | }
21 | Router.push(decodeURIComponent(next) || '/')
22 | }
23 | render() {
24 | return
25 | }
26 | }
27 |
28 | export default SignedIn
29 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | ---
2 | machine:
3 | environment:
4 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
5 | node:
6 | version: 8.2.0
7 |
8 | dependencies:
9 | override:
10 | - yarn global add codecov
11 | - yarn global add jsinspect
12 | - npm i -g npm-check-updates
13 | - yarn global add nsp
14 | - yarn --no-progress --no-emoji --ignore-engines
15 | cache_directories:
16 | - "node_modules"
17 | - ~/.yarn
18 | - ~/.cache/yarn
19 |
20 | test:
21 | override:
22 | - yarn check-deps
23 | - yarn check-security
24 | - yarn check-duplicate
25 | - yarn lint
26 | - yarn test --coverage
27 | - codecov
28 |
29 | deployment:
30 | production:
31 | branch: master
32 | heroku:
33 | appname: reactnative-gallery
--------------------------------------------------------------------------------
/components/NumberList.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components'
2 | import { tada } from 'react-animations'
3 | import { isFocus } from '../utils'
4 |
5 | const pulser = keyframes`${tada}`
6 |
7 | export const NumberList = styled.ol`
8 | padding: 15px;
9 | list-style: none;
10 | display: inline;
11 | background-color: 'rgba(255, 255, 255, 1)';
12 | opacity: 0.85;
13 | `
14 |
15 | export const NumberItem = styled.li`
16 | display: inline-block;
17 | padding: 15px;
18 | margin-bottom: 35px;
19 | border-radius: 15px;
20 | font-size: 20px;
21 | font-weight: bold;
22 | background-color: #ccc;
23 | background-color: ${isFocus('#00a651')};
24 | color: ${isFocus('#fff')};
25 | cursor: ${isFocus('pointer')};
26 | animation: ${isFocus(`1.5s ${pulser}`)};
27 | `
28 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Comment.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 |
22 |
25 | 0
26 |
27 |
28 | `;
29 |
--------------------------------------------------------------------------------
/server/es/__tests__/gif.test.js:
--------------------------------------------------------------------------------
1 | const gif = require('../gif')
2 |
3 | it('gif.createGifAsync', async () => {
4 | expect(await gif.createGifAsync({ id: 'test', key: 'value' })).toEqual({
5 | body: [
6 | { index: { _id: 'test', _index: 'gallery', _type: 'gif' } },
7 | { id: 'test', key: 'value' }
8 | ]
9 | })
10 | })
11 |
12 | it('gif.readAllGifAsync', async () => {
13 | expect(await gif.readAllGifAsync()).toEqual([])
14 | })
15 |
16 | // it('gif.getGifByIdAsync', async () => {
17 | // expect(await gif.getGifByIdAsync(1)).toEqual({
18 | // index: 'gallery',
19 | // type: 'gif',
20 | // id: 1
21 | // })
22 | // })
23 |
24 | it('gif.deleteGifByIdAsync', async () => {
25 | expect(await gif.deleteGifByIdAsync(1)).toEqual({
26 | body: [{ delete: { _id: '1', _index: 'gallery', _type: 'gif' } }]
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/components/Icon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ChevronRight from 'react-feather/dist/icons/chevron-right'
3 | import styled from 'styled-components'
4 |
5 | export const Icon = styled.div`
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | flex-direction: column;
10 | padding: 5px;
11 | cursor: ${({ pointer }) => (pointer ? 'pointer' : 'auto')};
12 | `
13 |
14 | Icon.Label = styled.span`
15 | color: #fff;
16 | font-size: 12px;
17 | font-weight: bold;
18 | `
19 |
20 | export const renderIcon = Comp => (
21 |
22 | )
23 |
24 | export const renderSmallIcon = (Comp, props) => (
25 |
26 | )
27 |
28 | export const Next = () => (
29 |
30 | )
31 |
--------------------------------------------------------------------------------
/utils/mailchimp.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const { getAsync } = require('./getAsync')
4 | const pathOr = require('ramda/src/pathOr')
5 |
6 | const {
7 | MAILCHIMP_API,
8 | MAILCHIMP_LIST_ID,
9 | MAILCHIMP_API_KEY,
10 | MAILCHIMP_MEMBER_COUNT_DEFAULT
11 | } = process.env
12 |
13 | const defaultValue = {
14 | stats: { member_count: MAILCHIMP_MEMBER_COUNT_DEFAULT }
15 | }
16 |
17 | const getMailchimpMemberCount = async () => {
18 | const result = await getAsync(
19 | {
20 | url: `${MAILCHIMP_API}lists/${MAILCHIMP_LIST_ID}`,
21 | headers: {
22 | Authorization: `apikey ${MAILCHIMP_API_KEY}`
23 | }
24 | },
25 | defaultValue
26 | )
27 | return pathOr(
28 | MAILCHIMP_MEMBER_COUNT_DEFAULT,
29 | ['stats', 'member_count'],
30 | result
31 | )
32 | }
33 |
34 | module.exports = getMailchimpMemberCount
35 |
--------------------------------------------------------------------------------
/components/GithubRibbon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 |
5 | const Ribbon = styled.img`
6 | position: absolute;
7 | top: 0;
8 | right: 0;
9 | border: 0;
10 | `
11 |
12 | const srcImg =
13 | 'https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png'
14 |
15 | const altTxt = 'Fork me on GitHub'
16 |
17 | const GithubRibbon = ({ src, alt }) => (
18 |
23 |
24 |
25 | )
26 |
27 | GithubRibbon.propTypes = {
28 | alt: PropTypes.string,
29 | src: PropTypes.string
30 | }
31 |
32 | GithubRibbon.defaultProps = {
33 | alt: altTxt,
34 | src: srcImg
35 | }
36 |
37 | export default GithubRibbon
38 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Notice.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 |
36 |
39 | Hover to play app video
40 |
41 |
42 | `;
43 |
--------------------------------------------------------------------------------
/components/__tests__/Wrapper.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Wrapper from '../Wrapper'
4 |
5 | const compToTest = (
6 |
7 | test
8 |
9 | )
10 |
11 | // const compToTest2 =
12 |
13 | // const compToTest3 =
14 |
15 | it('Wrapper can be created', () => {
16 | const comp = renderer.create(compToTest)
17 | expect(comp).toBeDefined()
18 | })
19 |
20 | // it(' toMatchSnapshot', () => {
21 | // const tree = renderer.create(compToTest).toJSON()
22 | // expect(tree).toMatchSnapshot()
23 | // })
24 |
25 | // it(' toMatchSnapshot', () => {
26 | // const tree = renderer.create(compToTest2).toJSON()
27 | // expect(tree).toMatchSnapshot()
28 | // })
29 |
30 | // it(' toMatchSnapshot', () => {
31 | // const tree = renderer.create(compToTest3).toJSON()
32 | // expect(tree).toMatchSnapshot()
33 | // })
34 |
--------------------------------------------------------------------------------
/components/Notice.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Info from 'react-feather/dist/icons/info'
3 | import styled from 'styled-components'
4 |
5 | const NoticeContainer = styled.div`
6 | display: flex;
7 | flex-shrink: 1;
8 | flex-grow: 0;
9 | flex-direction: row;
10 | margin-bottom: 15px;
11 | margin-left: 10px;
12 | padding: 2px;
13 | background: #7bc6cc;
14 | background: -webkit-linear-gradient(to right, #be93c5, #7bc6cc);
15 | background: linear-gradient(to right, #be93c5, #7bc6cc);
16 | border: 1px #fff solid;
17 | opacity: 0.9;
18 | color: #fff;
19 | border-radius: 15px;
20 | cursor: help;
21 | @media (max-width: 769px) {
22 | display: none;
23 | }
24 | `
25 |
26 | const NoticeText = styled.small`
27 | margin-right: 5px;
28 | margin-left: 5px;
29 | font-size: 12px;
30 | font-weight: bold;
31 | `
32 |
33 | const Notice = () => (
34 |
35 |
36 | Hover to play app video
37 |
38 | )
39 |
40 | export default Notice
41 |
--------------------------------------------------------------------------------
/utils/getAsync.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: 0 */
2 | const invariant = require('invariant')
3 | const LRU = require('lru-cache')
4 | const axios = require('axios')
5 |
6 | const cache = LRU({ max: 100, maxAge: 1000 * 60 * 60 })
7 |
8 | // type GetOptionsType = {
9 | // url: string,
10 | // headears?: Object,
11 | // timeout?: number
12 | // }
13 |
14 | // type defaultValueType = Object | string | number | boolean
15 | // url: string
16 | // }
17 |
18 | async function getAsync(options, defaultValue) {
19 | invariant(options.url, 'options.url undefined')
20 | invariant(defaultValue, 'defaultValue undefined')
21 |
22 | try {
23 | if (cache.has(options.url)) {
24 | return Promise.resolve(cache.get(options.url))
25 | }
26 | const { data } = await axios({
27 | method: 'GET',
28 | validateStatus: () => true,
29 | timeout: 1500,
30 | ...options
31 | })
32 | cache.set(options.url, data)
33 | return data
34 | } catch (error) {
35 | console.log(error)
36 | return Promise.resolve(defaultValue)
37 | }
38 | }
39 |
40 | module.exports = {
41 | getAsync
42 | }
43 |
--------------------------------------------------------------------------------
/components/__tests__/Gif.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Enzyme, { shallow } from 'enzyme'
4 | import Adapter from 'enzyme-adapter-react-16'
5 | import Gif from '../Gif'
6 |
7 | Enzyme.configure({ adapter: new Adapter() })
8 |
9 | it('Gif can be created', () => {
10 | const comp = renderer.create()
11 | expect(comp).toBeDefined()
12 | })
13 |
14 | it(' toMatchSnapshot', () => {
15 | const tree = renderer
16 | .create()
17 | .toJSON()
18 | expect(tree).toMatchSnapshot()
19 | })
20 |
21 | it(' toMatchSnapshot click', () => {
22 | const gif = shallow()
23 |
24 | const inst = gif.instance()
25 | inst.video = {
26 | play: () => 'play',
27 | pause: () => 'pause'
28 | }
29 |
30 | // mouseenter
31 | gif.simulate('mouseenter')
32 | expect(gif.state('play')).toBe(true)
33 |
34 | // mouseleave
35 | gif.simulate('mouseleave')
36 | expect(gif.state('play')).toBe(false)
37 | })
38 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright (c) 2018 reactnative.gallery
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
--------------------------------------------------------------------------------
/components/Wrapper.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 | import Footer from './Footer'
5 |
6 | const Wrapper = styled.section`
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | flex-direction: column;
11 | padding: 1em 3em;
12 | min-height: 100%;
13 | font-size: 20px;
14 | margin: 0;
15 | width: auto;
16 | height: auto;
17 | line-height: 1.42857;
18 | word-wrap: normal;
19 | color: #333;
20 | z-index: 1;
21 | @media (max-width: 481px) {
22 | font-size: 16px;
23 | padding: 0;
24 | line-height: 1.21;
25 | }
26 | `
27 |
28 | function Wrap({ children, footer }) {
29 | return (
30 |
31 | {children}
32 | {footer && }
33 |
34 | )
35 | }
36 |
37 | Wrap.defaultProps = {
38 | footer: false,
39 | children: Nothing to render
40 | }
41 |
42 | Wrap.propTypes = {
43 | children: PropTypes.oneOfType([
44 | PropTypes.arrayOf(PropTypes.element),
45 | PropTypes.element
46 | ]),
47 | footer: PropTypes.bool
48 | }
49 |
50 | export default Wrap
51 |
--------------------------------------------------------------------------------
/pages/player.js:
--------------------------------------------------------------------------------
1 | /* eslint
2 | jsx-a11y/click-events-have-key-events: 0,
3 | jsx-a11y/no-static-element-interactions: 0
4 | */
5 | import React from 'react'
6 | import PropTypes from 'prop-types'
7 | import pkg from '../package.json'
8 | import Gif from '../components/Gif'
9 |
10 | import { getGifByIdAsync } from '../utils/api'
11 |
12 | class Player extends React.PureComponent {
13 | goToDetail = () => {
14 | const { username, slug } = this.props
15 | if (process.browser && window) {
16 | window.open(`${pkg.website}/${username}/${slug}`)
17 | }
18 | }
19 |
20 | render() {
21 | const { id } = this.props
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 | }
29 |
30 | Player.propTypes = {
31 | id: PropTypes.string.isRequired,
32 | username: PropTypes.string.isRequired,
33 | slug: PropTypes.string.isRequired
34 | }
35 |
36 | Player.getInitialProps = async ({ req }) => {
37 | const { query: { id } } = req
38 | const { owner, slug } = await getGifByIdAsync(req, id)
39 | return { id, username: owner.id, slug }
40 | }
41 |
42 | export default Player
43 |
--------------------------------------------------------------------------------
/server/es/user.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const { compose, propOr } = require('ramda')
3 | const { catchP, log, then } = require('../../utils/pointFreePromise')
4 | const {
5 | bulkAsync,
6 | bulkIndex,
7 | getByIdFilterSourceAsync,
8 | getSourceAsync,
9 | addToPropAsync,
10 | removeFromPropAsync
11 | } = require('./')
12 |
13 | const { USER_INDEX, USER_TYPE } = process.env
14 |
15 | const indexUserBulkBase = bulkIndex(USER_INDEX, USER_TYPE)
16 |
17 | const indexUserAsync = compose(bulkAsync, indexUserBulkBase)
18 |
19 | const getUserFilter = getByIdFilterSourceAsync(USER_INDEX, USER_TYPE)
20 |
21 | // getUserLikesAsync :: id -> promise
22 | const getUserLikesAsync = compose(
23 | catchP(log),
24 | then(propOr([], 'likes')),
25 | getSourceAsync,
26 | getUserFilter('likes')
27 | )
28 |
29 | // addToUserLikesAsync :: username -> gifId -> promise
30 | const addToUserLikesAsync = addToPropAsync(USER_INDEX, USER_TYPE, 'likes')
31 |
32 | // removeFromUserLikesAsync :: username -> gifId -> promise
33 | const removeFromUserLikesAsync = removeFromPropAsync(
34 | USER_INDEX,
35 | USER_TYPE,
36 | 'likes'
37 | )
38 |
39 | module.exports = {
40 | indexUserAsync,
41 | getUserLikesAsync,
42 | addToUserLikesAsync,
43 | removeFromUserLikesAsync
44 | }
45 |
--------------------------------------------------------------------------------
/hocs/defaultPage.js:
--------------------------------------------------------------------------------
1 | /* eslint no-undef: 0, react/prop-types: 0 */
2 |
3 | import React from 'react'
4 | import Router from 'next/router'
5 |
6 | import { getUserFromServerCookie, getUserFromLocalCookie } from '../utils/auth'
7 | import Wrapper from '../components/Wrapper'
8 |
9 | const defaultPage = (Page) => {
10 | class DefaultPage extends React.Component {
11 | static async getInitialProps(ctx) {
12 | const user = process.browser
13 | ? getUserFromLocalCookie()
14 | : getUserFromServerCookie(ctx.req)
15 | const pageProps =
16 | Page.getInitialProps && (await Page.getInitialProps(ctx))
17 | return {
18 | ...pageProps,
19 | user,
20 | isAuthenticated: !!user
21 | }
22 | }
23 | componentDidMount() {
24 | window.addEventListener('storage', this.logout, false)
25 | }
26 |
27 | componentWillUnmount() {
28 | window.removeEventListener('storage', this.logout, false)
29 | }
30 |
31 | logout = (eve) => {
32 | if (eve.key === 'logout') {
33 | Router.push(`/?logout=${eve.newValue}`)
34 | }
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 | }
45 | return DefaultPage
46 | }
47 |
48 | export default defaultPage
49 |
--------------------------------------------------------------------------------
/components/__tests__/MailchimpForm.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Enzyme, { shallow } from 'enzyme'
4 | import Adapter from 'enzyme-adapter-react-16'
5 | import MailchimpForm from '../MailchimpForm'
6 |
7 | Enzyme.configure({ adapter: new Adapter() })
8 |
9 | it('MailchimpForm can be created', () => {
10 | const comp = renderer.create( {}}
16 | />)
17 | expect(comp).toBeDefined()
18 | })
19 |
20 | it(' toMatchSnapshot', () => {
21 | const tree = renderer
22 | .create( {}}
28 | />)
29 | .toJSON()
30 | expect(tree).toMatchSnapshot()
31 | })
32 |
33 | it(' simulate input change ', () => {
34 | // TODO: find a way to pass into onChange
35 | const email = 'cool@gmel.fr'
36 | const form = shallow( {}}
42 | />)
43 |
44 | form.simulate('change', { target: { value: 'cool@gmel.fr' } })
45 | expect(form.childAt(1).props().children[0].props.value).toBe('cool@gmel.fr')
46 | })
47 |
--------------------------------------------------------------------------------
/server/es/mappings/gallery_gif.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": {
3 | "id": {
4 | "type": "keyword"
5 | },
6 | "name": {
7 | "type": "text"
8 | },
9 | "slug": {
10 | "type": "text",
11 | "fields": {
12 | "keyword": {
13 | "type": "keyword",
14 | "ignore_above": 256
15 | }
16 | }
17 | },
18 | "shortDescription": {
19 | "type": "text"
20 | },
21 | "longDescription": {
22 | "type": "text"
23 | },
24 | "category": {
25 | "type": "keyword"
26 | },
27 | "like": {
28 | "type": "integer"
29 | },
30 | "numberOfView": {
31 | "type": "integer"
32 | },
33 | "comment": {
34 | "type": "nested"
35 | },
36 | "owner": {
37 | "type": "object",
38 | "properties": {
39 | "namespace": { "type": "keyword" },
40 | "id": { "type": "keyword" },
41 | "avatar": { "type": "keyword" }
42 | }
43 | },
44 | "githubLink": {
45 | "type": "keyword"
46 | },
47 | "expoLink": {
48 | "type": "keyword"
49 | },
50 | "appleStoreLink": {
51 | "type": "keyword"
52 | },
53 | "playStoreLink": {
54 | "type": "keyword"
55 | },
56 | "published": {
57 | "type": "boolean"
58 | },
59 | "uploadedAt": {
60 | "type": "date"
61 | },
62 | "createdAt": {
63 | "type": "date"
64 | },
65 | "updatedAt": {
66 | "type": "date"
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | /* eslint import/prefer-default-export: 0, no-console: 0 */
2 | const invariant = require('invariant')
3 | const chalk = require('chalk')
4 | const { curry } = require('ramda')
5 | const { website } = require('../package.json')
6 |
7 | const jsonToString = obj => JSON.stringify(obj, null, 2)
8 |
9 | const invariantsUndef = objToCheck =>
10 | Object.keys(objToCheck).forEach(k =>
11 | invariant(objToCheck[k], `"${k}" is undefined`))
12 |
13 | const logError = e =>
14 | console.error(chalk.red(`[ERROR] ${e && e.message}${e && e.stack}`))
15 |
16 | const logInfo = info => console.log(chalk.blue(`[INFO] ${info}`))
17 |
18 | const logWarning = warn => console.warn(chalk.yellow(`[WARN] ${warn}`))
19 |
20 | const isProd = (nodeEnv = process.env.NODE_ENV) => nodeEnv === 'production'
21 |
22 | const isFocus = cssValue => props => (props.focus ? cssValue : null)
23 |
24 | const now = () => new Date()
25 |
26 | const tmatches = curry((toMatch, funObj) => {
27 | const identity = x => x
28 | const fn = funObj[typeof toMatch] || identity
29 | return fn(toMatch)
30 | })
31 |
32 | const baseApi = (req) => {
33 | const scheme = isProd() ? 'https' : 'http'
34 | const host = req && req.headers && req.headers.host
35 | const origin =
36 | !host && window && window.location.origin !== 'null'
37 | ? window.location.origin
38 | : undefined
39 | const url = host ? `${scheme}://${host}` : origin || website
40 | return url
41 | }
42 |
43 | module.exports = {
44 | isProd,
45 | isFocus,
46 | now,
47 | baseApi,
48 | invariantsUndef,
49 | logError,
50 | logInfo,
51 | logWarning,
52 | jsonToString,
53 | tmatches
54 | }
55 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/Gif.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
16 |
19 |
22 |
25 |
55 |
56 |
68 |
69 | `;
70 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/MailchimpForm.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` toMatchSnapshot 1`] = `
4 |
7 |
10 |
64 |
67 | Join
68 |
69 | 1
70 |
71 | members
72 |
73 |
74 | `;
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Awesome [reactnative.gallery](https://reactnative.gallery) website
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 👉 [](https://reactnative.gallery) 👈
10 | [](https://spectrum.chat/reactnative-gallery)
11 | [](https://github.com/styled-components/styled-components)
12 | ##
13 | ## 🗣 Pitch
14 |
15 | **Reactnative.gallery** is a website where you can visualize apps and open source components as videos. Created by a react-native developer who realized that a way to visually share applications and simple mobile developments was sorely lacking, in particular for animations, navigation transitions, navigation drawers or simply smooth, fluid applications.
16 |
17 | It is impossible to show these aspects with simple screenshots. And installing the app just to see it is too much hassle.
18 |
19 | **Reactnative.gallery** makes it possible to not only **visualize apps at a glance** using videos, but also to describe the app, categorize it, do a search and above all **share it with the rest of the community**.
20 |
21 | GitHub is loaded with react-native repositories containing one or more animated gifs of apps or components, which are unfortunately assimilated to any media type.
22 |
23 | For open-source developers, you can login with GitHub and your animated gifs will be **automatically recognized and shared**, and then can receive feedback from the community (comments and the number of views and likes are displayed).
24 |
25 | For those who are searching for a particular component, you can search by category or popularity, or simply do a full-text search to find what you are looking for.
26 |
27 | ## 🏗 ToDo list
28 |
29 | * [ ] Edit video
30 |
31 | ## Hire an expert!
32 | Looking for a ReactNative freelance expert with more than 12 years experience? Contact me from my [website](https://xaviercarpentier.com)!
33 |
--------------------------------------------------------------------------------
/pages/about.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Info from 'react-feather/dist/icons/info'
4 | import Paragraph from '../components/Paragraph'
5 | import defaultPage from '../hocs/defaultPage'
6 | import Container from '../components/Container'
7 | import Speech from '../components/Speech'
8 |
9 | const Logo = styled.img`
10 | border-radius: 100px;
11 | float: right;
12 | margin-left: 15px;
13 | margin-bottom: 15px;
14 | @media (max-width: 769px) {
15 | display: none;
16 | }
17 | `
18 |
19 | const About = () => (
20 |
21 |
22 |
23 |
24 |
25 | Reactnative.gallery is a website where you can
26 | visualize apps and open source components as videos.
27 |
Created by a react-native developer who realized that a way to
28 | visually share applications and simple mobile developments was sorely
29 | lacking, in particular for animations, navigation transitions,
30 | navigation drawers or simply smooth, fluid applications.
31 |
32 |
33 | It is impossible to show these aspects with simple screenshots. And
34 | installing the app just to see it is too much hassle.
35 |
36 |
37 | Reactnative.gallery makes it possible to not only{' '}
38 | visualize apps at a glance using videos, but also to
39 | describe the app, categorize it, do a search and above all{' '}
40 | share it with the rest of the community.
41 |
42 |
43 | GitHub is loaded with react-native repositories containing one or more
44 | animated gifs of apps or components, which are unfortunately assimilated
45 | to any media type.
46 |
47 |
48 | For open-source developers, you can login with GitHub and your animated
49 | gifs will be automatically recognized and shared, and
50 | then can receive feedback from the community (comments and the number of
51 | views and likes are displayed).
52 |
53 |
54 | For those who are searching for a particular component, you can search
55 | by category or popularity, or simply do a full-text search to find what
56 | you are looking for.
57 |
58 |
59 |
60 | )
61 |
62 | export default defaultPage(About)
63 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require('newrelic')
2 | require('dotenv').config()
3 |
4 | const express = require('express')
5 | const { getAccessTokenAsync } = require('../utils/api')
6 | const next = require('next')
7 | const fs = require('fs')
8 | const { parse } = require('url')
9 | const pathMatch = require('path-match')
10 | const helmet = require('helmet')
11 | const bodyParser = require('body-parser')
12 | const cookieParser = require('cookie-parser')
13 | const { join } = require('path')
14 | const { setEsEndpoints } = require('./es/endpoints')
15 | const getMailchimpMemberCount = require('../utils/mailchimp')
16 |
17 | const port = parseInt(process.env.PORT, 10) || 3000
18 | const dev = process.env.NODE_ENV !== 'production'
19 | const app = next({ dev })
20 | const handle = app.getRequestHandler()
21 |
22 | const route = pathMatch()
23 | const matchDetail = route('/:username/:slug')
24 |
25 | app.prepare().then(() => {
26 | const server = express()
27 |
28 | server.use(helmet())
29 | server.use(bodyParser.json())
30 | server.use(cookieParser())
31 | server.disable('x-powered-by')
32 | server.enable('trust proxy')
33 |
34 | server.head('/tk', async (req, res) => {
35 | const { GFYCAT_CLIENT_ID, GFYCAT_CLIENT_SECRET } = process.env
36 | const token = await getAccessTokenAsync(
37 | GFYCAT_CLIENT_ID,
38 | GFYCAT_CLIENT_SECRET
39 | )
40 | res.header('X-TK', token)
41 | return res.send()
42 | })
43 |
44 | setEsEndpoints(server)
45 |
46 | server.get('/robots.txt', (req, res) => {
47 | const path = join(__dirname, 'robots.txt')
48 | res.type('text/plain')
49 | res.send(fs.readFileSync(path, { encoding: 'utf8' }))
50 | })
51 |
52 | server.get('/stats/member_count', async (req, res) => {
53 | res.type('text/plain')
54 | const membersCount = await getMailchimpMemberCount()
55 | res.send(`${membersCount}`)
56 | })
57 |
58 | server.get('/sitemap.xml', (req, res) => {
59 | const path = join(__dirname, 'sitemap.xml')
60 | res.type('application/xml')
61 | res.send(fs.readFileSync(path, { encoding: 'utf8' }))
62 | })
63 |
64 | server.get('*', (req, res) => {
65 | const { pathname, query } = parse(req.url, true)
66 | const params = matchDetail(pathname)
67 | if (params === false) {
68 | res.header('x-frame-options', 'ALLOWALL')
69 | handle(req, res)
70 | return
71 | }
72 | app.render(req, res, '/appdetail', Object.assign(params, query))
73 | })
74 |
75 | server.listen(port, (err) => {
76 | if (err) throw err
77 | // eslint-disable-next-line
78 | console.log(`> Ready on http://localhost:${port}`)
79 | })
80 | })
81 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | /* eslint react/no-danger: 0, max-len: 0 */
2 | import React from 'react'
3 | import Document, { Head, Main, NextScript } from 'next/document'
4 | import { ServerStyleSheet } from 'styled-components'
5 | import '../reset.css'
6 | import { isProd } from '../utils'
7 |
8 | const scripts = [
9 | isProd() &&
10 | '(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,"script","//www.google-analytics.com/analytics.js","ga");ga("create", "UA-109685698-1", "auto");ga("send", "pageview");'
11 | ]
12 |
13 | const stylesheets = ['https://fonts.googleapis.com/css?family=Open+Sans']
14 |
15 | export default class MyDocument extends Document {
16 | static getInitialProps({ renderPage }) {
17 | const sheet = new ServerStyleSheet()
18 | const page = renderPage(App => props =>
19 | sheet.collectStyles())
20 | const styleTags = sheet.getStyleElement()
21 | return { ...page, styleTags }
22 | }
23 |
24 | render() {
25 | return (
26 |
27 |
28 | React Native Gallery
29 |
30 |
34 |
39 |
44 |
49 |
50 |
51 |
52 | {stylesheets.map(css => (
53 |
54 | ))}
55 | {this.props.styleTags}
56 |
57 |
58 |
59 |
60 | {scripts.map(script => (
61 |
62 | ))}
63 |
64 |
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/utils/__tests__/utils.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | jsonToString,
3 | invariantsUndef,
4 | isFocus,
5 | tmatches,
6 | baseApi,
7 | logError,
8 | logInfo,
9 | logWarning,
10 | now
11 | } from '../'
12 |
13 | it('should transform json to pretty string', () => {
14 | expect(jsonToString({
15 | id: 1,
16 | list: [{ key: 0, value: 'value', value1: 'value1' }],
17 | bool: true,
18 | num: 1,
19 | str: 'string',
20 | key1: undefined,
21 | key2: null
22 | })).toBe(`{
23 | "id": 1,
24 | "list": [
25 | {
26 | "key": 0,
27 | "value": "value",
28 | "value1": "value1"
29 | }
30 | ],
31 | "bool": true,
32 | "num": 1,
33 | "str": "string",
34 | "key2": null
35 | }`)
36 | })
37 |
38 | it('should throw exception according undefined key', () => {
39 | try {
40 | invariantsUndef({
41 | id: undefined
42 | })
43 | } catch (error) {
44 | expect(error.message).toBe('"id" is undefined')
45 | }
46 | })
47 |
48 | it('should not throw exception according defined key', () => {
49 | invariantsUndef({
50 | id: 1
51 | })
52 | expect(true).toBe(true)
53 | })
54 |
55 | it('should return value if focus props', () => {
56 | expect(isFocus('#fff')({ focus: true })).toBe('#fff')
57 | })
58 |
59 | it('should return null if not focus props', () => {
60 | expect(isFocus('#fff')({ focus: false })).toBe(null)
61 | })
62 |
63 | it('should match type', () => {
64 | const date = new Date()
65 | expect(tmatches('a string')({ string: x => x })).toBe('a string')
66 | expect(tmatches(1)({ number: x => x })).toBe(1)
67 | expect(tmatches(true)({ boolean: x => !x })).toBe(false)
68 | expect(tmatches(date)({ object: x => x })).toBe(date)
69 | expect(tmatches({ id: 0 })({ object: x => x.id })).toBe(0)
70 | expect(tmatches({ id: 0 })({ object: undefined })).toEqual({ id: 0 })
71 | })
72 |
73 | it('should return correct url', () => {
74 | expect(baseApi({ headers: { host: 'domain.com' } })).toBe('http://domain.com')
75 | expect(baseApi(null)).toBe('https://reactnative.gallery')
76 | })
77 |
78 | it('should log info', () => {
79 | logInfo('test')
80 | expect(true).toBe(true)
81 | })
82 |
83 | it('should log warn', () => {
84 | logWarning('test')
85 | expect(true).toBe(true)
86 | })
87 |
88 | it('should log warn', () => {
89 | logError()
90 | expect(true).toBe(true)
91 | logError('test 1')
92 | expect(true).toBe(true)
93 | logError(new Error('test 2'))
94 | expect(true).toBe(true)
95 | })
96 |
97 | it('should give now date', () => {
98 | expect(typeof now).toBe('function')
99 | expect(typeof now()).toBeDefined()
100 | expect(typeof now()).toBe('object')
101 | expect(now().getTime()).toBeLessThanOrEqual(new Date().getTime())
102 | })
103 |
--------------------------------------------------------------------------------
/server/es/gif.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const compose = require('ramda/src/compose')
3 | const pathOr = require('ramda/src/pathOr')
4 | const propOr = require('ramda/src/propOr')
5 | const head = require('ramda/src/head')
6 | const curry = require('ramda/src/curry')
7 | const map = require('ramda/src/map')
8 | const filter = require('ramda/src/filter')
9 | const propEq = require('ramda/src/propEq')
10 | const { then, catchP, log } = require('../../utils/pointFreePromise')
11 | const {
12 | bulkAsync,
13 | getAllAsync,
14 | getByIdAsync,
15 | getByKeywordAsync,
16 | bulkIndex,
17 | bulkDelete,
18 | bulkUpdate,
19 | incrementPropAsync,
20 | decrementPropAsync,
21 | getSourceAsync
22 | } = require('./')
23 |
24 | const { GALLERY_INDEX, GALLERY_TYPE } = process.env
25 |
26 | const indexGifBulkBase = bulkIndex(GALLERY_INDEX, GALLERY_TYPE)
27 |
28 | const deleteGifBulkBase = bulkDelete(GALLERY_INDEX, GALLERY_TYPE)
29 |
30 | const updateGifBulkBase = bulkUpdate(GALLERY_INDEX, GALLERY_TYPE)
31 |
32 | const getGifByIdAsync = id =>
33 | compose(catchP(log), getSourceAsync, getByIdAsync)(
34 | GALLERY_INDEX,
35 | GALLERY_TYPE,
36 | id
37 | )
38 |
39 | const deleteGifByIdAsync = compose(bulkAsync, deleteGifBulkBase)
40 |
41 | const updateGifByIdAsync = compose(bulkAsync, updateGifBulkBase)
42 |
43 | const createGifAsync = compose(bulkAsync, indexGifBulkBase)
44 |
45 | const getGifBySlugAsync = slug =>
46 | compose(
47 | catchP(log),
48 | then(propOr({}, '_source')),
49 | then(head),
50 | then(pathOr([], ['hits', 'hits'])),
51 | getByKeywordAsync
52 | )(GALLERY_INDEX, GALLERY_TYPE, 'slug.keyword', slug)
53 |
54 | const readAllGifAsync = () =>
55 | compose(
56 | catchP(log),
57 | then(filter(propEq('published', true))),
58 | then(map(propOr({}, '_source'))),
59 | then(pathOr([], ['hits', 'hits'])),
60 | then(head),
61 | then(propOr([], ['responses'])),
62 | curry(getAllAsync)
63 | )(GALLERY_INDEX, GALLERY_TYPE, 30)
64 |
65 | const incrementNumberOfViewAsync = id =>
66 | compose(catchP(log), incrementPropAsync)(
67 | GALLERY_INDEX,
68 | GALLERY_TYPE,
69 | 'numberOfView',
70 | id
71 | )
72 |
73 | const incrementLikeAsync = id =>
74 | compose(catchP(log), incrementPropAsync)(
75 | GALLERY_INDEX,
76 | GALLERY_TYPE,
77 | 'like',
78 | id
79 | )
80 |
81 | const decrementLikeAsync = id =>
82 | compose(catchP(log), decrementPropAsync)(
83 | GALLERY_INDEX,
84 | GALLERY_TYPE,
85 | 'like',
86 | id
87 | )
88 |
89 | module.exports = {
90 | readAllGifAsync,
91 | getGifByIdAsync,
92 | createGifAsync,
93 | deleteGifByIdAsync,
94 | updateGifByIdAsync,
95 | getGifBySlugAsync,
96 | incrementNumberOfViewAsync,
97 | incrementLikeAsync,
98 | decrementLikeAsync
99 | }
100 |
--------------------------------------------------------------------------------
/server/es/endpoints.js:
--------------------------------------------------------------------------------
1 | const {
2 | readAllGifAsync,
3 | createGifAsync,
4 | getGifByIdAsync,
5 | updateGifByIdAsync,
6 | getGifBySlugAsync,
7 | incrementNumberOfViewAsync,
8 | incrementLikeAsync,
9 | decrementLikeAsync
10 | } = require('./gif')
11 |
12 | const {
13 | indexUserAsync,
14 | getUserLikesAsync,
15 | addToUserLikesAsync,
16 | removeFromUserLikesAsync
17 | } = require('./user')
18 | const { now } = require('../../utils')
19 |
20 | module.exports = {
21 | setEsEndpoints(server) {
22 | // gif
23 | server.get('/gifs', async (_, res) => res.send(await readAllGifAsync()))
24 | server.get('/gifs/:id', async ({ params: { id } }, res) =>
25 | res.send(await getGifByIdAsync(id)))
26 | server.put('/gifs/:id', async (req, res) => {
27 | const { id } = req.params
28 | await updateGifByIdAsync({ id, ...req.body })
29 | res.sendStatus(200)
30 | })
31 | server.get('/gifs/slug/:slug', async ({ params: { slug } }, res) =>
32 | res.send(await getGifBySlugAsync(slug)))
33 |
34 | server.post('/gifs/', async (req, res) => {
35 | const { id } = req.params
36 | await createGifAsync({ id, ...req.body })
37 | res.sendStatus(201)
38 | })
39 | server.put('/gifs/increment-number-of-view/:id', async (req, res) => {
40 | const { id } = req.params
41 | await incrementNumberOfViewAsync(id)
42 | res.sendStatus(204)
43 | })
44 |
45 | // user
46 | server.put('/users/:username', async (req, res) => {
47 | const { nickname } = JSON.parse(req.cookies.user)
48 | const user = req.body
49 | const { username } = req.params
50 | if (!user || username !== nickname || user.nickname !== nickname) {
51 | res.sendStatus(400)
52 | } else {
53 | const {
54 | email, email_verified, picture, name
55 | } = user
56 | const timestamp = now()
57 | await indexUserAsync({
58 | id: username,
59 | emailVerified: email_verified,
60 | email,
61 | published: true,
62 | picture,
63 | name,
64 | createdAt: timestamp,
65 | updatedAt: timestamp
66 | })
67 | res.sendStatus(204)
68 | }
69 | })
70 |
71 | // like
72 | server.get(
73 | '/users/:username/likes',
74 | async ({ params: { username } }, res) =>
75 | res.send(await getUserLikesAsync(username))
76 | )
77 |
78 | server.put(
79 | '/gif/:gifId/user/:username/like',
80 | async ({ params: { gifId, username } }, res) => {
81 | await incrementLikeAsync(gifId)
82 | await addToUserLikesAsync(username, gifId)
83 | return res.sendStatus(204)
84 | }
85 | )
86 |
87 | server.put(
88 | '/gif/:gifId/user/:username/unlike',
89 | async ({ params: { gifId, username } }, res) => {
90 | await decrementLikeAsync(gifId)
91 | await removeFromUserLikesAsync(username, gifId)
92 | res.sendStatus(204)
93 | }
94 | )
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactnative-gallery",
3 | "description": "Visualize React Native apps at a glance",
4 | "keywords": ["react-native", "react native example", "gallery"],
5 | "version": "1.0.0",
6 | "main": "index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/ReactNativeGallery/reactnative-gallery-web"
10 | },
11 | "author":
12 | "Xavier Carpentier (http://xaviercarpentier.com/)",
13 | "license": "MIT",
14 | "website": "https://reactnative.gallery",
15 | "scripts": {
16 | "lint": "./node_modules/.bin/eslint .",
17 | "test": "./node_modules/jest/bin/jest.js",
18 | "check-deps": "ncu -p -e 2",
19 | "check-security": "nsp check --reporter summary",
20 | "check-duplicate": "jsinspect --ignore '.next'",
21 | "dev": "node server/index.js",
22 | "precommit": "lint-staged && yarn test",
23 | "build": "next build",
24 | "start": "NODE_ENV=production node server/index.js",
25 | "heroku-postbuild": "next build"
26 | },
27 | "dependencies": {
28 | "auth0-lock": "11.7.2",
29 | "axios": "0.18.0",
30 | "babel-plugin-styled-components": "1.5.1",
31 | "babel-plugin-transform-define-file": "1.3.2",
32 | "body-parser": "1.18.3",
33 | "chalk": "2.4.1",
34 | "cookie-parser": "1.4.3",
35 | "csurf": "1.9.0",
36 | "dotenv": "6.0.0",
37 | "elasticsearch": "15.1.1",
38 | "express": "4.16.3",
39 | "helmet": "3.12.1",
40 | "history": "4.7.2",
41 | "hoist-non-react-statics": "2.5.5",
42 | "invariant": "2.2.4",
43 | "isomorphic-fetch": "2.2.1",
44 | "js-cookie": "2.2.0",
45 | "jwt-decode": "2.2.0",
46 | "lru-cache": "4.1.3",
47 | "newrelic": "4.3.0",
48 | "next": "6.1.1",
49 | "normalize.css": "8.0.0",
50 | "path-match": "1.2.4",
51 | "prop-types": "15.6.2",
52 | "ramda": "0.25.0",
53 | "raw-loader": "0.5.1",
54 | "rc-progress": "2.2.5",
55 | "react": "16.4.1",
56 | "react-animations": "1.0.0",
57 | "react-dom": "16.4.2",
58 | "react-feather": "1.1.1",
59 | "styled-components": "3.3.3",
60 | "styled-css-grid": "0.11.0",
61 | "uuid": "3.3.2"
62 | },
63 | "devDependencies": {
64 | "@babel/core": "7.0.0-beta.46",
65 | "babel-cli": "7.0.0-beta.3",
66 | "babel-core": "^7.0.0-0",
67 | "babel-eslint": "8.2.3",
68 | "babel-jest": "22.4.3",
69 | "babel-preset-env": "7.0.0-beta.3",
70 | "babel-preset-react": "7.0.0-beta.3",
71 | "enzyme": "3.3.0",
72 | "enzyme-adapter-react-16": "1.1.1",
73 | "eslint": "^4.9.0",
74 | "eslint-config-airbnb": "16.1.0",
75 | "eslint-plugin-import": "^2.7.0",
76 | "eslint-plugin-jsx-a11y": "^6.0.2",
77 | "eslint-plugin-react": "^7.4.0",
78 | "husky": "0.14.3",
79 | "jest": "22.4.2",
80 | "lint-staged": "7.2.0",
81 | "plop": "2.0.0",
82 | "prettier": "1.12.1",
83 | "react-test-renderer": "16.4.0",
84 | "regenerator-runtime": "0.11.1"
85 | },
86 | "lint-staged": {
87 | "**/*.js": ["yarn lint --fix", "git add"]
88 | },
89 | "jest": {
90 | "transform": {
91 | "^.+\\.js$": "babel-jest"
92 | },
93 | "setupFiles": ["./testSetup.js"]
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/components/MailchimpForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 | import Check from 'react-feather/dist/icons/check'
5 | import Hideable from './Hideable'
6 | import CleanHr from './CleanHr'
7 |
8 | const Container = styled.div`
9 | padding-top: 30px;
10 | padding-bottom: 30px;
11 | width: 100%;
12 | margin: auto 0;
13 | text-align: center;
14 | `
15 |
16 | const MailchimpForm = styled.form`
17 | margin: 0 auto;
18 | width: 100%;
19 | max-width: 250px;
20 | display: flex;
21 | flex-direction: row;
22 | `
23 |
24 | const SmallLabel = styled.small`
25 | color: white;
26 | font-size: 14px;
27 | margin-left: -16px;
28 | `
29 | export const MailchimpInput = styled.input`
30 | border-width: 0;
31 | border-radius: 5px 0 0 5px;
32 | font-size: 14px;
33 | height: 50px;
34 | display: block;
35 | width: 100%;
36 | height: 36px;
37 | padding: 6px 0 6px 12px;
38 | font-size: 14px;
39 | line-height: 1.42857143;
40 | color: #555;
41 | background-color: #fff;
42 | background-image: none;
43 | border: 1px solid #ccc;
44 | overflow: hidden;
45 | text-overflow: ellipsis;
46 | white-space: nowrap;
47 | &:required {
48 | box-shadow: none;
49 | }
50 | &:invalid {
51 | box-shadow: none;
52 | }
53 | `
54 |
55 | export const MailchimpButton = styled.button`
56 | display: block;
57 | margin: 0 auto;
58 | border-width: 0;
59 | border-radius: 0 5px 5px 0;
60 | font-size: 14px;
61 | padding-right: 20px;
62 | padding-left: 20px;
63 | height: 50px;
64 | background: #76b852;
65 | background: -webkit-linear-gradient(to right, #8dc26f, #76b852);
66 | background: linear-gradient(to right, #8dc26f, #76b852);
67 | color: #fff;
68 | line-height: 0.5em;
69 | `
70 |
71 | const Mailchimp = ({
72 | action, type, onChange, email, memberCount
73 | }) => (
74 |
75 |
76 |
83 | onChange(e.target.value)}
91 | />
92 |
93 |
94 |
95 | JOIN
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Join {memberCount} members
105 |
106 |
107 | )
108 |
109 | Mailchimp.defaultProps = {
110 | email: '',
111 | memberCount: process.env.MAILCHIMP_MEMBER_COUNT_DEFAULT
112 | }
113 |
114 | Mailchimp.propTypes = {
115 | action: PropTypes.string.isRequired,
116 | memberCount: PropTypes.string,
117 | type: PropTypes.string.isRequired,
118 | onChange: PropTypes.func.isRequired,
119 | email: PropTypes.string
120 | }
121 |
122 | export default Mailchimp
123 |
--------------------------------------------------------------------------------
/utils/auth.js:
--------------------------------------------------------------------------------
1 | /* eslint
2 | camelcase: 0
3 | */
4 | import jwtDecode from 'jwt-decode'
5 | import uuid from 'uuid'
6 | import Cookie from 'js-cookie'
7 | import axios from 'axios'
8 | import qs from 'querystring'
9 |
10 | const getLock = (options) => {
11 | // eslint-disable-next-line
12 | const Auth0Lock = require('auth0-lock').default
13 | return new Auth0Lock(process.env.AUTH_ID, process.env.AUTH_DOMAIN, options)
14 | }
15 |
16 | const getBaseUrl = () => `${window.location.protocol}//${window.location.host}`
17 |
18 | export const setSecret = secret => Cookie.set('secret', secret, { expires: 90 })
19 |
20 | const getOptions = (container, params) => {
21 | const secret = uuid.v4()
22 | setSecret(secret)
23 | return {
24 | container,
25 | closable: false,
26 | theme: {
27 | logo: `${getBaseUrl()}/static/images/logo.png`
28 | },
29 | languageDictionary: {
30 | title: 'Log in'
31 | },
32 | auth: {
33 | responseType: 'token id_token',
34 | redirectUrl: `${getBaseUrl()}/signed-in?${qs.stringify(params)}`,
35 | params: {
36 | scope: 'openid profile email',
37 | state: secret
38 | }
39 | }
40 | }
41 | }
42 |
43 | const getQueryParams = () => {
44 | const params = {}
45 | window.location.href.replace(
46 | /([^(?|#)=&]+)(=([^&]*))?/g,
47 | ($0, $1, $2, $3) => {
48 | params[$1] = $3
49 | }
50 | )
51 | return params
52 | }
53 |
54 | export const extractInfoFromHash = () => {
55 | if (!process.browser) {
56 | return undefined
57 | }
58 | const {
59 | id_token, state, access_token, ...rest
60 | } = getQueryParams()
61 | return {
62 | token: id_token,
63 | secret: state,
64 | access_token,
65 | ...rest
66 | }
67 | }
68 |
69 | export const getUserFromServerCookie = (req) => {
70 | if (!req.headers.cookie) {
71 | return undefined
72 | }
73 | const jwtCookie = req.headers.cookie
74 | .split(';')
75 | .find(c => c.trim().startsWith('jwt='))
76 | if (!jwtCookie) {
77 | return undefined
78 | }
79 | const jwt = jwtCookie.split('=')[1]
80 | return jwtDecode(jwt)
81 | }
82 |
83 | export const getUserFromLocalCookie = () => Cookie.getJSON('user')
84 |
85 | export const show = (container, params) =>
86 | getLock(getOptions(container, params)).show()
87 |
88 | export const logout = () => getLock().logout({ returnTo: getBaseUrl() })
89 |
90 | export const setTokenAsync = token =>
91 | new Promise((resolve) => {
92 | if (!process.browser) {
93 | return resolve()
94 | }
95 | const user = jwtDecode(token)
96 | Cookie.set('user', user, { expires: 90 })
97 | Cookie.set('jwt', token, { expires: 90 })
98 | return resolve(user)
99 | })
100 |
101 | export const unsetToken = () => {
102 | if (!process.browser) {
103 | return
104 | }
105 | Cookie.remove('jwt')
106 | Cookie.remove('user')
107 | Cookie.remove('secret')
108 |
109 | // to support logging out from all windows
110 | window.localStorage.setItem('logout', Date.now())
111 | }
112 |
113 | export const isAuthenticated = () => {
114 | const expiresAt = JSON.parse(Cookie.get('expires_at'))
115 | return new Date().getTime() < expiresAt
116 | }
117 |
118 | export const checkSecret = secret => Cookie.get('secret') === secret
119 |
120 | export const saveUserAsync = user =>
121 | axios.put(`${getBaseUrl()}/users/${user.nickname}`, user)
122 |
--------------------------------------------------------------------------------
/components/SocialBar.js:
--------------------------------------------------------------------------------
1 | /* eslint max-len: 0 */
2 | import React from 'react'
3 | import styled from 'styled-components'
4 | import PropTypes from 'prop-types'
5 | import twitter from 'react-feather/dist/icons/twitter'
6 | import facebook from 'react-feather/dist/icons/facebook'
7 | import linkedin from 'react-feather/dist/icons/linkedin'
8 | import mail from 'react-feather/dist/icons/mail'
9 | import { renderSmallIcon } from './Icon'
10 | import pkg from '../package.json'
11 |
12 | const defaultHref = pkg.website
13 | const defaultTitle = pkg.description
14 |
15 | const color = {
16 | facebook: { normal: '#3b5998', hover: '#2d4373' },
17 | twitter: { normal: '#55acee', hover: '#2795e9' },
18 | linkedin: { normal: '#0077b5', hover: '#046293' },
19 | mail: { normal: '#be93c5', hover: '#9b78a0' }
20 | }
21 |
22 | const getColor = ({ provider }) => color[provider].normal
23 | const getHoverColor = ({ provider }) => color[provider].hover
24 |
25 | const Container = styled.div`
26 | position: fixed;
27 | padding: 0;
28 | margin: 0;
29 | top: 20% !important;
30 | bottom: auto;
31 | width: 3pc;
32 | z-index: 100020;
33 | background: none;
34 | left: 0;
35 | `
36 |
37 | const ShareButton = styled.div`
38 | display: flex;
39 | height: 30px;
40 | justify-content: center;
41 | align-items: center;
42 | padding: 0.5em 0.75em;
43 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
44 | color: #fff;
45 | opacity: 0.9;
46 | background-color: ${getColor};
47 | &:hover {
48 | background-color: ${getHoverColor};
49 | }
50 | `
51 |
52 | const ShareLink = styled.a`
53 | display: inline-block;
54 | text-decoration: none;
55 | color: #fff;
56 | margin: 0;
57 | `
58 |
59 | const Facebook = () => (
60 | {renderSmallIcon(facebook)}
61 | )
62 |
63 | const Twitter = () => (
64 | {renderSmallIcon(twitter)}
65 | )
66 |
67 | const LinkedIn = () => (
68 | {renderSmallIcon(linkedin)}
69 | )
70 |
71 | const Mail = () => (
72 | {renderSmallIcon(mail)}
73 | )
74 |
75 | const Share = props => (
76 |
77 | {props.render(props)}
78 |
79 | )
80 | Share.propTypes = {
81 | render: PropTypes.func.isRequired,
82 | href: PropTypes.string.isRequired
83 | }
84 |
85 | const FacebookLink = ({ href }) => (
86 | }
89 | />
90 | )
91 | FacebookLink.propTypes = {
92 | href: PropTypes.string
93 | }
94 | FacebookLink.defaultProps = {
95 | href: defaultHref
96 | }
97 |
98 | const TwitterLink = ({ href, title }) => (
99 | }
102 | />
103 | )
104 | TwitterLink.propTypes = {
105 | href: PropTypes.string,
106 | title: PropTypes.string
107 | }
108 | TwitterLink.defaultProps = {
109 | href: defaultHref,
110 | title: defaultTitle
111 | }
112 |
113 | const LinkedInLink = ({ href, title }) => (
114 | }
117 | />
118 | )
119 | LinkedInLink.propTypes = {
120 | href: PropTypes.string,
121 | title: PropTypes.string
122 | }
123 | LinkedInLink.defaultProps = {
124 | href: defaultHref,
125 | title: defaultTitle
126 | }
127 |
128 | const MailLink = ({ href, title }) => (
129 | }
132 | />
133 | )
134 | MailLink.propTypes = {
135 | href: PropTypes.string,
136 | title: PropTypes.string
137 | }
138 | MailLink.defaultProps = {
139 | href: defaultHref,
140 | title: defaultTitle
141 | }
142 |
143 | const SocialBar = props => (
144 |
145 |
146 |
147 |
148 |
149 |
150 | )
151 | SocialBar.propTypes = {
152 | href: PropTypes.string.isRequired,
153 | title: PropTypes.string.isRequired
154 | }
155 |
156 | export default SocialBar
157 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Grid, Cell } from 'styled-css-grid'
3 | import Head from 'next/head'
4 | import PropTypes from 'prop-types'
5 | import Title from '../components/Title'
6 | import Subtitle from '../components/Subtitle'
7 | import Gif from '../components/Gif'
8 | import Notice from '../components/Notice'
9 | import Hideable from '../components/Hideable'
10 | import MailchimpForm from '../components/MailchimpForm'
11 | import GithubRibbon from '../components/GithubRibbon'
12 | import { getGifsAsync, memberCountAsync } from '../utils/api'
13 | import defaultPage from '../hocs/defaultPage'
14 | import pkg from '../package.json'
15 | import SocialBar from '../components/SocialBar'
16 |
17 | class Home extends React.PureComponent {
18 | static propTypes = {
19 | gifs: PropTypes.arrayOf(PropTypes.object).isRequired,
20 | type: PropTypes.string,
21 | email: PropTypes.string,
22 | action: PropTypes.string,
23 | memberCount: PropTypes.string
24 | }
25 |
26 | static defaultProps = {
27 | type: 'developer',
28 | email: '',
29 | action: process.env.MAILCHIMP_ACTION,
30 | memberCount: process.env.MAILCHIMP_MEMBER_COUNT_DEFAULT
31 | }
32 |
33 | static getInitialProps = async ({ query, req }) => {
34 | const { utm_campaign, email } = query
35 | const gifs = await getGifsAsync(req)
36 | const memberCount = await memberCountAsync(req)
37 | return {
38 | type: utm_campaign || 'developer',
39 | gifs,
40 | email,
41 | memberCount
42 | }
43 | }
44 |
45 | constructor(props) {
46 | super(props)
47 | this.state = { email: props.email }
48 | }
49 |
50 | render() {
51 | const {
52 | gifs, type, action, memberCount
53 | } = this.props
54 | return (
55 |
56 |
57 |
React Native Gallery
58 |
63 |
68 |
73 |
78 |
83 |
84 |
90 |
91 | Show and tell for React Native developers
92 | What are you working on?
93 |
94 | React Native Gallery is where developers get visibility{' '}
95 | {type !== 'developer' && 'and hired'}
96 |
97 | |
98 |
99 |
103 | this.setState({ email: mel })}
109 | />
110 |
111 |
112 |
113 |
114 |
115 |
116 | {gifs &&
117 | gifs.filter(g => !g.rotate).map(gif => (
118 |
119 |
124 | |
125 | ))}
126 |
127 | |
128 |
129 |
130 |
131 |
132 |
133 |
134 | )
135 | }
136 | }
137 |
138 | export default defaultPage(Home)
139 |
--------------------------------------------------------------------------------
/utils/api.js:
--------------------------------------------------------------------------------
1 | require('isomorphic-fetch')
2 | const { now, baseApi } = require('.')
3 |
4 | const axios = require('axios')
5 | const { memoize } = require('ramda')
6 |
7 | const axiosInstance = axios.create({
8 | baseURL: process.env.BASE_GIF_API,
9 | timeout: 30000,
10 | headers: { 'Content-Type': 'application/json' },
11 | validateStatus: () => true
12 | })
13 |
14 | const tokenData = {
15 | token: undefined
16 | }
17 |
18 | const getToken = async () => {
19 | const { headers } = await axios.head('/tk')
20 | tokenData.token = headers['x-tk']
21 | return tokenData.token
22 | }
23 |
24 | const getAccessTokenAsync = async (clientId, clientSecret) => {
25 | const result = await axios.post(`${process.env.BASE_GIF_API}oauth/token`, {
26 | grant_type: 'client_credentials',
27 | client_id: clientId,
28 | client_secret: clientSecret
29 | })
30 | if (result.status === 200 && result.data) {
31 | return result.data.access_token
32 | }
33 | return null
34 | }
35 |
36 | const getStatusAsync = async (id) => {
37 | const result = await axiosInstance.get(
38 | `gfycats/fetch/status/${id}`,
39 | tokenData.token && {
40 | headers: {
41 | Authorization: `Bearer ${tokenData.token}`
42 | }
43 | }
44 | )
45 |
46 | if (result.status < 400 && result.data) {
47 | return result.data
48 | }
49 | return null
50 | }
51 |
52 | const getGifInfo = memoize(async (id) => {
53 | const token = await getAccessTokenAsync(
54 | process.env.GFYCAT_CLIENT_ID,
55 | process.env.GFYCAT_CLIENT_SECRET
56 | )
57 | const result = await axiosInstance.get(`gfycats/${id}`, {
58 | headers: {
59 | Authorization: `Bearer ${token}`
60 | }
61 | })
62 | return result.data.gfyItem
63 | })
64 |
65 | const requestGifKeyAsync = async () => {
66 | const token = await getToken()
67 |
68 | const result = await axiosInstance.post(
69 | 'gfycats',
70 | { noMd5: true },
71 | {
72 | headers: {
73 | Authorization: `Bearer ${token}`
74 | }
75 | }
76 | )
77 |
78 | if (result.status < 400 && result.data) {
79 | return result.data.gfyname
80 | }
81 | return null
82 | }
83 |
84 | const uploadAsync = (id, file, onUploadProgress) =>
85 | axios.put(`${process.env.BASE_GIF_UPLOAD}${id}`, file, {
86 | headers: { 'Content-Type': file.type },
87 | onUploadProgress
88 | })
89 |
90 | const createGifAsync = (id, base, timestamp = now()) =>
91 | axios.post(`${base || ''}/gifs/`, {
92 | id,
93 | uploadedAt: timestamp,
94 | like: 0,
95 | numberOfView: 0,
96 | published: true,
97 | createdAt: timestamp,
98 | updatedAt: timestamp
99 | })
100 |
101 | const getGifsAsync = async (req) => {
102 | const result = await fetch(`${baseApi(req)}/gifs`)
103 | return result.json()
104 | }
105 |
106 | const getGifBySlugAsync = async (req, slug) => {
107 | const result = await fetch(`${baseApi(req)}/gifs/slug/${slug}`)
108 | return result.json()
109 | }
110 |
111 | const getGifByIdAsync = async (req, id) => {
112 | const result = await fetch(`${baseApi(req)}/gifs/${id}`)
113 | return result.json()
114 | }
115 |
116 | const getUserLikesAsync = async (req, id) => {
117 | const result = await fetch(`${baseApi(req)}/users/${id}/likes`)
118 | return result.json()
119 | }
120 |
121 | const putLikeAsync = async (req, username, gifId) => {
122 | const result = await fetch(
123 | `${baseApi(req)}/gif/${gifId}/user/${username}/like`,
124 | {
125 | method: 'put'
126 | }
127 | )
128 | return result
129 | }
130 |
131 | const putUnlikeAsync = async (req, username, gifId) => {
132 | const result = await fetch(
133 | `${baseApi(req)}/gif/${gifId}/user/${username}/unlike`,
134 | {
135 | method: 'put'
136 | }
137 | )
138 | return result
139 | }
140 |
141 | const putIncrementNumberOfViewAsync = async (req, id) => {
142 | if (process.env.NODE_ENV !== 'production') {
143 | return Promise.resolve()
144 | }
145 | const result = await fetch(
146 | `${baseApi(req)}/gifs/increment-number-of-view/${id}`,
147 | {
148 | method: 'put'
149 | }
150 | )
151 | return result
152 | }
153 |
154 | const memberCountAsync = async (req) => {
155 | const result = await fetch(`${baseApi(req)}/stats/member_count`)
156 | return result.text()
157 | }
158 |
159 | module.exports = {
160 | getToken,
161 | getStatusAsync,
162 | requestGifKeyAsync,
163 | uploadAsync,
164 | createGifAsync,
165 | getGifsAsync,
166 | getGifBySlugAsync,
167 | getGifInfo,
168 | getAccessTokenAsync,
169 | getGifByIdAsync,
170 | putIncrementNumberOfViewAsync,
171 | getUserLikesAsync,
172 | putLikeAsync,
173 | putUnlikeAsync,
174 | memberCountAsync
175 | }
176 |
--------------------------------------------------------------------------------
/components/Gif/index.js:
--------------------------------------------------------------------------------
1 | /* eslint react/forbid-prop-types: 0 */
2 |
3 | import React, { Component } from 'react'
4 | import PropTypes from 'prop-types'
5 | import styled from 'styled-components'
6 | import Smartphone from './Smartphone'
7 | import GifContainer from './GifContainer'
8 | import Play from './Play'
9 | import { Button, ButtonContainer } from './Button'
10 |
11 | export const Sizer = styled.div`
12 | position: relative;
13 | padding-bottom: 180%;
14 | `
15 |
16 | const Source = ({ mediatype, id, baseSourceGifGiant }) => (
17 |
21 | )
22 | Source.propTypes = {
23 | mediatype: PropTypes.string.isRequired,
24 | id: PropTypes.string.isRequired,
25 | baseSourceGifGiant: PropTypes.string.isRequired
26 | }
27 |
28 | class Gif extends Component {
29 | static propTypes = {
30 | gifId: PropTypes.string.isRequired,
31 | username: PropTypes.string,
32 | slug: PropTypes.string,
33 | minWidth: PropTypes.number,
34 | autoplay: PropTypes.bool,
35 | styles: PropTypes.object,
36 | rotate: PropTypes.bool
37 | }
38 |
39 | static defaultProps = {
40 | minWidth: undefined,
41 | autoplay: false,
42 | styles: {},
43 | username: undefined,
44 | slug: undefined,
45 | rotate: false
46 | }
47 |
48 | state = {
49 | play: this.props.autoplay,
50 | buttonHover: false,
51 | buttonClicked: false
52 | }
53 |
54 | onMouseEnterHandler = () => {
55 | if (this.props.autoplay) {
56 | return
57 | }
58 | this.play()
59 | this.setState({ play: true })
60 | }
61 |
62 | onMouseLeaveHandler = () => {
63 | if (this.props.autoplay) {
64 | return
65 | }
66 | this.pause()
67 | this.setState({ play: false })
68 | }
69 |
70 | onClick = () => {
71 | const { username, slug } = this.props
72 | window.location.href = `/${username}/${slug}`
73 | }
74 |
75 | pause = () => {
76 | if (this.playPromise !== undefined && this.playPromise.then) {
77 | this.playPromise.then(() => this.video.pause())
78 | }
79 | }
80 |
81 | playPromise = undefined
82 | play = () => {
83 | this.playPromise = this.video.play()
84 | }
85 |
86 | render() {
87 | const {
88 | gifId,
89 | username,
90 | slug,
91 | minWidth,
92 | autoplay,
93 | styles,
94 | rotate
95 | } = this.props
96 | return (
97 |
109 |
110 |
111 |
112 |
144 |
145 |
146 |
161 |
162 | )
163 | }
164 | }
165 |
166 | export default Gif
167 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 | import Github from 'react-feather/dist/icons/github'
5 | import Minus from 'react-feather/dist/icons/minus'
6 | import LinkedIn from 'react-feather/dist/icons/linkedin'
7 | import Facebook from 'react-feather/dist/icons/facebook'
8 | import Info from 'react-feather/dist/icons/info'
9 | import Home from 'react-feather/dist/icons/home'
10 | import Heart from 'react-feather/dist/icons/heart'
11 | import Briefcase from 'react-feather/dist/icons/briefcase'
12 | import { getStargazersCountAsync, getFullNameFormUrl } from '../utils/github'
13 | import pkg from '../package.json'
14 | import Hideable from './Hideable'
15 |
16 | const Footer = styled.section`
17 | position: absolute;
18 | width: 100%;
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 | padding: 25px 0;
23 | margin-top: 100px;
24 | background-color: transparent;
25 | min-height: 100px;
26 | z-index: 1000;
27 | text-align: center;
28 | flex-shrink: 0;
29 | @media (max-width: 481px) {
30 | padding: 0;
31 | margin: 35px auto;
32 | }
33 | `
34 |
35 | const LinkStyl = styled.a`
36 | color: #fff;
37 | display: flex;
38 | text-decoration-line: none;
39 | flex-direction: column;
40 | justify-content: center;
41 | align-items: center;
42 | min-width: 50px;
43 | &:visited: {
44 | color: #fff;
45 | }
46 | @media (max-width: 481px) {
47 | min-width: 35px;
48 | }
49 | `
50 |
51 | const Stats = styled.div`
52 | color: #fff,
53 | text-decoration-line: 'none';
54 | font-size: 10px;
55 | @media (max-width: 481px) {
56 | display: none;
57 | }
58 | `
59 |
60 | const Link = ({ href, children, target }) => (
61 |
66 | {children}
67 |
68 | )
69 | Link.propTypes = {
70 | href: PropTypes.string.isRequired,
71 | target: PropTypes.string,
72 | children: PropTypes.oneOfType([
73 | PropTypes.arrayOf(PropTypes.element),
74 | PropTypes.element
75 | ]).isRequired
76 | }
77 | Link.defaultProps = {
78 | target: '_blank'
79 | }
80 |
81 | const HorizontalSeparator = () => (
82 |
83 |
89 |
90 | )
91 |
92 | const LastLine = styled.small`
93 | position: absolute;
94 | bottom: 0;
95 | padding: 15px;
96 | display: flex;
97 | justify-content: center;
98 | align-items: center;
99 | color: white;
100 | z-index: 1000;
101 | a {
102 | color: white;
103 | }
104 | `
105 |
106 | const wait = 'wait...'
107 |
108 | class Foot extends React.Component {
109 | state = { stargazersCount: 0 }
110 | componentDidMount() {
111 | this.getStargazersCount()
112 | }
113 | getStargazersCount = async () => {
114 | const fullName =
115 | pkg.repository &&
116 | pkg.repository.url &&
117 | getFullNameFormUrl(pkg.repository.url)
118 | const stargazersCount = await getStargazersCountAsync(fullName)
119 | this.setState({ stargazersCount })
120 | }
121 | render() {
122 | const { stargazersCount } = this.state
123 | return (
124 |
125 |
172 |
173 | )
174 | }
175 | }
176 |
177 | export default Foot
178 |
--------------------------------------------------------------------------------
/server/es/__tests__/es.test.js:
--------------------------------------------------------------------------------
1 | const es = require('../')
2 | const mapping = require('../mappings/gallery_gif.json')
3 |
4 | it('es.pingAsync', async () => {
5 | expect(await es.pingAsync()).toBe(true)
6 | })
7 |
8 | it('es.mappingPath', () => {
9 | expect(es.mappingPath('t', 'ti', 'ta')).toBe('t/mappings/ti_ta.json')
10 | })
11 |
12 | it('es.isLocalMappingExist', () => {
13 | expect(es.isLocalMappingExist('gallery', 'gif')).toBe(true)
14 | })
15 |
16 | it('es.getLocalMapping', () => {
17 | expect(es.getLocalMapping('gallery', 'gif')).toEqual(mapping)
18 | })
19 |
20 | it('es.isIndexExistAsync', async () => {
21 | expect(await es.isIndexExistAsync('gallery')).toBe(false)
22 | })
23 |
24 | it('es.isIndexExistAsync undefined index', async () => {
25 | try {
26 | await es.isIndexExistAsync()
27 | } catch (error) {
28 | expect(error.message).toBe('index should be defined')
29 | }
30 | })
31 |
32 | it('es.isTypeExistAsync', async () => {
33 | expect(await es.isTypeExistAsync('gallery', 'gif')).toBe(false)
34 | })
35 |
36 | it('es.isTypeExistAsync index undefined', async () => {
37 | try {
38 | await es.isTypeExistAsync(undefined, 'gif')
39 | } catch (error) {
40 | expect(error.message).toBe('index should be defined')
41 | }
42 | })
43 |
44 | it('es.isTypeExistAsync type undefined', async () => {
45 | try {
46 | await es.isTypeExistAsync('gallery')
47 | } catch (error) {
48 | expect(error.message).toBe('type should be defined')
49 | }
50 | })
51 |
52 | it('es.createIndexAsync', async () => {
53 | expect(await es.createIndexAsync('index')).toEqual({})
54 | })
55 |
56 | it('es.createIndexAsync index undefined', async () => {
57 | try {
58 | await es.createIndexAsync()
59 | } catch (error) {
60 | expect(error.message).toBe('index should be defined')
61 | }
62 | })
63 |
64 | it('es.createTypeAsync', async () => {
65 | expect(await es.createTypeAsync('index', 'type', {})).toEqual({})
66 | })
67 |
68 | it('es.createTypeAsync index undefined', async () => {
69 | try {
70 | await es.createTypeAsync()
71 | } catch (error) {
72 | expect(error.message).toBe('index should be defined')
73 | }
74 | })
75 |
76 | it('es.createTypeAsync type undefined', async () => {
77 | try {
78 | await es.createTypeAsync('index')
79 | } catch (error) {
80 | expect(error.message).toBe('type should be defined')
81 | }
82 | })
83 |
84 | it('es.createTypeAsync mapping undefined', async () => {
85 | try {
86 | await es.createTypeAsync('index', 'type')
87 | } catch (error) {
88 | expect(error.message).toBe('mapping should be defined')
89 | }
90 | })
91 |
92 | it('es.bulkOperation', () => {
93 | expect(es.bulkOperation('index', 'indexTest', 'typeTest', {
94 | key: 'value',
95 | id: 'test'
96 | })).toEqual([
97 | { index: { _index: 'indexTest', _type: 'typeTest', _id: 'test' } },
98 | { key: 'value', id: 'test' }
99 | ])
100 | })
101 |
102 | it('es.bulkAsync, bulkables should be defined', async () => {
103 | try {
104 | await es.bulkAsync()
105 | } catch (error) {
106 | expect(error.message).toBe('bulkables should be defined')
107 | }
108 | })
109 |
110 | it('es.bulkAsync, bulkables should not be empty', async () => {
111 | try {
112 | await es.bulkAsync([])
113 | } catch (error) {
114 | expect(error.message).toBe('bulkables should not be empty')
115 | }
116 | })
117 |
118 | it('es.bulkAsync, bulkables length should be an even number', async () => {
119 | try {
120 | await es.bulkAsync([{}])
121 | } catch (error) {
122 | expect(error.message).toBe('bulkables length should be an even number')
123 | }
124 | })
125 |
126 | it('es.bulkAsync success', async () => {
127 | const bulk = es.bulkOperation('indexTest', 'typeTest', {
128 | key: 'value',
129 | id: 'idTest'
130 | })
131 | expect(await es.bulkAsync(bulk)).toEqual({ body: bulk })
132 | })
133 |
134 | it('es.getAllAsync success', async () => {
135 | expect(await es.getAllAsync('testIndex', 'testType')).toEqual({
136 | body: [
137 | { index: 'testIndex', type: 'testType' },
138 | { size: 10, query: { match_all: {} } }
139 | ]
140 | })
141 | })
142 |
143 | it('es.getByIdAsync success', async () => {
144 | expect(await es.getByIdAsync('testIndex', 'testType', 'id')).toEqual({
145 | index: 'testIndex',
146 | type: 'testType',
147 | id: 'id'
148 | })
149 | })
150 |
151 | it('es.compact', () => {
152 | expect(es.compact([
153 | null,
154 | undefined,
155 | {},
156 | { doc: { prop: 'value' } },
157 | { doc: { prop: 'value2' } }
158 | ])).toEqual([{ doc: { prop: 'value' } }, { doc: { prop: 'value2' } }])
159 | })
160 |
161 | it('es.idify', () => {
162 | expect(es.idify(1)).toBe('1')
163 | expect(es.idify('id')).toBe('id')
164 | expect(es.idify({ id: 'id' })).toBe('id')
165 | expect(es.idify({ _id: 'id' })).toBe('id')
166 | })
167 |
168 | it('es.initIndexTypeAsync', async () => {
169 | expect(await es.initIndexTypeAsync('gallery', 'gif')).toEqual({})
170 | })
171 |
172 | it('es.initIndexTypeAsync', async () => {
173 | expect(await es.initIndexTypeAsync('yes', 'yes')).toEqual({
174 | message: 'succeeded, nothing created'
175 | })
176 | })
177 |
--------------------------------------------------------------------------------
/server/es/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: 0 */
2 | require('dotenv').config()
3 | const {
4 | compose, curry, isEmpty, is, filter
5 | } = require('ramda')
6 | const elasticsearch = require('elasticsearch')
7 | const invariant = require('invariant')
8 | const fs = require('fs')
9 | const { tmatches } = require('../../utils')
10 | const { then } = require('../../utils/pointFreePromise')
11 | const propOr = require('ramda/src/propOr')
12 |
13 | const { ES_URL } = process.env
14 |
15 | const client = new elasticsearch.Client({
16 | host: ES_URL
17 | })
18 |
19 | const OPE = {
20 | DELETE: 'delete',
21 | INDEX: 'index',
22 | UPDATE: 'index'
23 | }
24 |
25 | const pingAsync = () => client.ping({ requestTimeout: 1000 })
26 |
27 | const isIndexExistAsync = index =>
28 | invariant(index, 'index should be defined') ||
29 | client.indices.exists({ index })
30 |
31 | const isTypeExistAsync = (index, type) =>
32 | invariant(index, 'index should be defined') ||
33 | invariant(type, 'type should be defined') ||
34 | client.indices.existsType({ index, type })
35 |
36 | const createIndexAsync = index =>
37 | invariant(index, 'index should be defined') ||
38 | client.indices.create({ index })
39 |
40 | const createTypeAsync = (index, type, body) =>
41 | invariant(index, 'index should be defined') ||
42 | invariant(type, 'type should be defined') ||
43 | invariant(body, 'mapping should be defined') ||
44 | client.indices.putMapping({ index, type, body })
45 |
46 | // eslint-disable-next-line
47 | const mappingPath = curry(
48 | (dir, idx, typ) => `${dir}/mappings/${idx}_${typ}.json`)
49 |
50 | const isLocalMappingExist = (index, type) =>
51 | compose(fs.existsSync, mappingPath(__dirname))(index, type)
52 |
53 | const readFile = filename => fs.readFileSync(filename, { encoding: 'utf8' })
54 |
55 | const getLocalMapping = (index, type) =>
56 | compose(JSON.parse, readFile, mappingPath(__dirname))(index, type)
57 |
58 | const initIndexTypeAsync = async (index, type) => {
59 | await pingAsync()
60 | const isIndexExist = await isIndexExistAsync(index)
61 | if (!isIndexExist) {
62 | await createIndexAsync(index)
63 | }
64 | const isTypeExist = await isTypeExistAsync(index, type)
65 | if (!isTypeExist) {
66 | let mapping = {}
67 | if (isLocalMappingExist(index, type)) {
68 | mapping = getLocalMapping(index, type)
69 | }
70 | return createTypeAsync(index, type, mapping)
71 | }
72 | return { message: 'succeeded, nothing created' }
73 | }
74 |
75 | const compact = filter(item => !isEmpty(item) && is(Object, item))
76 |
77 | const idify = idlike =>
78 | tmatches(idlike)({
79 | number: x => `${x}`,
80 | string: x => x,
81 | object: x => `${x.id || x._id}`
82 | })
83 |
84 | const bulkOperation = curry((ope, index, type, doc) =>
85 | compact([
86 | {
87 | [ope]: {
88 | _index: index,
89 | _type: type,
90 | _id: idify(doc)
91 | }
92 | },
93 | doc
94 | ]))
95 |
96 | const bulkIndex = bulkOperation(OPE.INDEX)
97 |
98 | const bulkUpdate = bulkOperation(OPE.UPDATE)
99 |
100 | const bulkDelete = bulkOperation(OPE.DELETE)
101 |
102 | const bulkAsync = bulkables =>
103 | invariant(bulkables, 'bulkables should be defined') ||
104 | invariant(bulkables.length, 'bulkables should not be empty') ||
105 | client.bulk({ body: bulkables })
106 |
107 | const getAllAsync = (index, type, size = 10) =>
108 | invariant(index, 'index should be defined') ||
109 | invariant(type, 'type should be defined') ||
110 | client.msearch({
111 | body: [{ index, type }, { size, query: { match_all: {} } }]
112 | })
113 |
114 | const getByIdAsync = curry((index, type, id) =>
115 | client.get({
116 | index,
117 | type,
118 | id
119 | }))
120 |
121 | const getByIdFilterSourceAsync = curry((index, type, source, id) =>
122 | client.get({
123 | index,
124 | type,
125 | id,
126 | _source: source
127 | }))
128 |
129 | const getByKeywordAsync = curry((index, type, keywordName, keywordValue) =>
130 | client.search({
131 | index,
132 | type,
133 | q: `${keywordName}:${keywordValue}`
134 | }))
135 |
136 | const incrementPropAsync = curry((index, type, counterName, id) =>
137 | client.update({
138 | index,
139 | type,
140 | id,
141 | body: {
142 | script: `ctx._source.${counterName} += 1`
143 | }
144 | }))
145 |
146 | const decrementPropAsync = curry((index, type, counterName, id) =>
147 | client.update({
148 | id,
149 | type,
150 | index,
151 | body: {
152 | script: `ctx._source.${counterName} -= 1`
153 | }
154 | }))
155 |
156 | const addToPropAsync = curry((index, type, key, id, keywordValue) =>
157 | client.update({
158 | index,
159 | type,
160 | id,
161 | body: {
162 | script: {
163 | inline: `(ctx._source.${key} = ctx._source.${key} ?: []).add(params.v)`,
164 | params: {
165 | v: keywordValue
166 | }
167 | }
168 | }
169 | }))
170 |
171 | const removeFromPropAsync = curry((index, type, key, id, keyValue) =>
172 | client.update({
173 | index,
174 | type,
175 | id,
176 | body: {
177 | script: {
178 | source: `ctx._source.${key}.removeAll(Collections.singleton(params.v))`,
179 | params: {
180 | v: keyValue
181 | }
182 | }
183 | }
184 | }))
185 |
186 | const getSourceAsync = then(propOr({}, '_source'))
187 |
188 | module.exports = {
189 | pingAsync,
190 | mappingPath,
191 | isLocalMappingExist,
192 | getLocalMapping,
193 | initIndexTypeAsync,
194 | isIndexExistAsync,
195 | isTypeExistAsync,
196 | createIndexAsync,
197 | createTypeAsync,
198 | getAllAsync,
199 | getByIdAsync,
200 | compact,
201 | bulkOperation,
202 | bulkAsync,
203 | bulkIndex,
204 | bulkUpdate,
205 | bulkDelete,
206 | idify,
207 | getByKeywordAsync,
208 | incrementPropAsync,
209 | decrementPropAsync,
210 | addToPropAsync,
211 | getByIdFilterSourceAsync,
212 | getSourceAsync,
213 | removeFromPropAsync
214 | }
215 |
--------------------------------------------------------------------------------
/pages/upload.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import { Grid, Cell } from 'styled-css-grid'
4 | import UploadIcon from 'react-feather/dist/icons/upload'
5 | import Film from 'react-feather/dist/icons/film'
6 | import FolderPlus from 'react-feather/dist/icons/folder-plus'
7 | import { Circle } from 'rc-progress'
8 | import { renderIcon, Next } from '../components/Icon'
9 |
10 | import { isFocus } from '../utils'
11 | import VerticalyCentered from '../components/VerticalyCentered'
12 | import { NumberItem, NumberList } from '../components/NumberList'
13 | import Subtitle from '../components/Subtitle'
14 | import { Sizer } from '../components/Gif'
15 | import Smartphone from '../components/Gif/Smartphone'
16 | import GifContainer from '../components/Gif/GifContainer'
17 | import defaultPage from '../hocs/defaultPage'
18 | import {
19 | requestGifKeyAsync,
20 | uploadAsync,
21 | getStatusAsync,
22 | createGifAsync
23 | } from '../utils/api'
24 |
25 | const Label = styled.label`
26 | cursor: pointer;
27 | background-color: #00a651;
28 | color: #fff;
29 | padding: 10px;
30 | opacity: 0.85;
31 | &:hover {
32 | background-color: #00a65f;
33 | background-color: ${isFocus('transparent')};
34 | }
35 | background-color: ${isFocus('transparent')};
36 | color: ${isFocus('#333')};
37 | padding-left: ${isFocus('0px')};
38 | `
39 |
40 | const Img = styled.img`
41 | position: absolute;
42 | width: 100%;
43 | height: 100%;
44 | top: 0;
45 | left: 0;
46 | `
47 |
48 | const Empty = styled.div`
49 | position: absolute;
50 | width: 100%;
51 | height: 100%;
52 | top: 0;
53 | left: 0;
54 | `
55 |
56 | const Placeholder = styled.span`
57 | display: flex;
58 | flex-direction: column;
59 | justify-content: center;
60 | align-items: center;
61 | height: 100%;
62 | width: 100%;
63 | color: #777;
64 | background-color: #fff;
65 | `
66 |
67 | const Status = styled.p`
68 | color: #333;
69 | text-align: center;
70 | font-size: 14px;
71 | `
72 |
73 | const Video = styled.video`
74 | position: absolute;
75 | width: 100%;
76 | height: 100%;
77 | top: 0;
78 | left: 0;
79 | `
80 |
81 | class Upload extends Component {
82 | constructor(props) {
83 | super(props)
84 | this.state = {
85 | file: undefined,
86 | isGif: true,
87 | isVideo: false,
88 | preview: false,
89 | percentCompleted: 0,
90 | status: { task: 'uploading' }
91 | }
92 | }
93 |
94 | handleChange = (e) => {
95 | const file = e.target.files[0]
96 | const isGif = file.type === 'image/gif'
97 | this.setState({
98 | file,
99 | isGif,
100 | isVideo: !isGif,
101 | preview: false
102 | })
103 | }
104 | upload = async () => {
105 | const id = await requestGifKeyAsync()
106 | await uploadAsync(id, this.state.file, (progressEvent) => {
107 | const loaded = progressEvent.loaded * 100
108 | const percentCompleted = Math.floor(loaded / progressEvent.total)
109 | this.setState({
110 | percentCompleted:
111 | this.state.percentCompleted > percentCompleted
112 | ? this.state.percentCompleted
113 | : percentCompleted
114 | })
115 | })
116 |
117 | await createGifAsync(id)
118 |
119 | const intervalID = setInterval(async () => {
120 | const status = await getStatusAsync(id)
121 | const progress = Number(status.progress || 0.01) * 100
122 | this.setState({
123 | status,
124 | percentCompleted: progress > 100 ? 100 : progress
125 | })
126 | if (
127 | !status ||
128 | (status.task && status.task === 'complete') ||
129 | status.task === 'error'
130 | ) {
131 | if (status.task === 'complete') {
132 | window.location.href = '/'
133 | }
134 | clearInterval(intervalID)
135 | }
136 | }, 7000)
137 | }
138 |
139 | render() {
140 | const {
141 | file,
142 | isGif,
143 | isVideo,
144 | preview,
145 | percentCompleted,
146 | status
147 | } = this.state
148 | return (
149 |
150 | Upload Video & Gif
151 |
162 |
171 | {!file && (
172 | this.inputFile && this.inputFile.click()}
174 | cursorPointer
175 | >
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | )}
186 | {file &&
187 | percentCompleted === 0 && (
188 |
189 |
190 |
191 | {isGif && }
192 | {isVideo && (
193 |
196 | )}
197 |
198 |
199 | )}
200 |
201 | {percentCompleted > 0 && (
202 | this.inputFile && this.inputFile.click()}
205 | >
206 |
207 |
208 |
209 |
210 |
211 |
218 |
219 | {status && (
220 |
221 | {status.task === 'NotFoundo'
222 | ? 'preparing encoding'
223 | : status.task}
224 | {' '}
225 | {percentCompleted}%
226 |
227 | )}
228 |
229 |
230 |
231 |
232 | )}
233 | |
234 |
247 |
248 |
249 |
265 |
266 |
267 |
268 | {renderIcon(UploadIcon)} upload it
269 |
270 |
271 | |
272 |
273 |
274 | )
275 | }
276 | }
277 |
278 | Upload.defaultProps = {}
279 |
280 | Upload.getInitialProps = () => ({})
281 |
282 | export default defaultPage(Upload)
283 |
--------------------------------------------------------------------------------
/pages/appdetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Router from 'next/router'
4 | import styled from 'styled-components'
5 | import Head from 'next/head'
6 | import Briefcase from 'react-feather/dist/icons/briefcase'
7 | import ViewIcon from '../components/ViewIcon'
8 | import Subtitle from '../components/Subtitle'
9 | import Love from '../components/Love'
10 | import Octicon from '../components/Octicon'
11 | import defaultPage from '../hocs/defaultPage'
12 | import SocialBar from '../components/SocialBar'
13 | import MailchimpForm from '../components/MailchimpForm'
14 | import {
15 | getGifBySlugAsync,
16 | getGifInfo,
17 | putIncrementNumberOfViewAsync,
18 | getUserLikesAsync,
19 | putLikeAsync,
20 | putUnlikeAsync,
21 | memberCountAsync
22 | } from '../utils/api'
23 | import { getStargazersCountAsync, getFullNameFormUrl } from '../utils/github'
24 | import Gif from '../components/Gif'
25 | import pkg from '../package.json'
26 | import { getUserFromLocalCookie, getUserFromServerCookie } from '../utils/auth'
27 | import { getJobsAsync } from '../utils/jobs'
28 |
29 | const CountBar = styled.div`
30 | display: flex;
31 | flex-direction: row;
32 | justify-content: space-around;
33 | align-items: center;
34 | margin-top: ${({ rotate }) => (Boolean(rotate) ? '-100px' : '17px')};
35 | min-width: 250px;
36 | `
37 |
38 | const Author = styled.h5`
39 | display: block;
40 | text-align: center;
41 | color: #fff;
42 | margin-top: -1em;
43 | margin-bottom: 1em;
44 | `
45 |
46 | const Job = styled.div`
47 | margin-top: 45px;
48 | color: #fff;
49 | font-size: 16px;
50 | display: flex;
51 | flex-direction: column;
52 | justify-content: center;
53 | align-items: center;
54 | text-align: center;
55 | a {
56 | color: white;
57 | }
58 | `
59 |
60 | const getTitle = (name, username) => `${name} by @${username}`
61 |
62 | const getImageMeta = id =>
63 | `${process.env.BASE_SOURCE_GIF_THUMBS}${id}-size_restricted.gif`
64 |
65 | const getUnsecureImageMeta = id =>
66 | `${process.env.BASE_SOURCE_GIF_THUMBS_UNSECURE}${id}-size_restricted.gif`
67 |
68 | const getVideoMeta = id =>
69 | `${process.env.BASE_SOURCE_GIF_THUMBS}${id}-mobile.mp4`
70 |
71 | const getUnsecureVideoMeta = id =>
72 | `${process.env.BASE_SOURCE_GIF_THUMBS_UNSECURE}${id}-mobile.mp4`
73 |
74 | const updateLoveAsync = async (user, gifId, alreadyLiked) => {
75 | if (user) {
76 | if (!alreadyLiked) {
77 | await putLikeAsync(undefined, user.nickname, gifId)
78 | } else {
79 | await putUnlikeAsync(undefined, user.nickname, gifId)
80 | }
81 | } else {
82 | const next = window.location.pathname
83 | Router.push({
84 | pathname: '/sign-in',
85 | query: { next }
86 | })
87 | }
88 | }
89 |
90 | class AppDetail extends React.Component {
91 | state = {
92 | checked: this.props.checked,
93 | like: this.props.like,
94 | email: undefined
95 | }
96 | render() {
97 | const {
98 | id,
99 | slug,
100 | owner,
101 | numberOfView,
102 | name,
103 | shortDescription,
104 | category,
105 | username,
106 | originalUrl,
107 | githubLink,
108 | width,
109 | height,
110 | stars,
111 | user,
112 | rotate,
113 | type,
114 | action,
115 | memberCount,
116 | job
117 | } = this.props
118 | const { checked, like, email } = this.state
119 | return (
120 |
121 |
122 | {getTitle(name, username)}
123 |
128 |
133 |
134 |
135 |
136 |
140 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
168 |
169 | {name}
170 | by @{username}
171 |
179 |
180 |
181 | {
184 | updateLoveAsync(user, id, checked)
185 | this.setState({
186 | checked: !checked,
187 | like: checked ? like - 1 : like + 1
188 | })
189 | }}
190 | checked={checked}
191 | />
192 | {githubLink && }
193 |
194 |
198 | this.setState({ email: mel })}
204 | />
205 |
206 |
207 |
208 | job
209 |
210 |
211 | {job.title}
212 |
213 |
214 |
215 | )
216 | }
217 | }
218 |
219 | AppDetail.propTypes = {
220 | id: PropTypes.string.isRequired,
221 | name: PropTypes.string.isRequired,
222 | slug: PropTypes.string.isRequired,
223 | username: PropTypes.string.isRequired,
224 | originalUrl: PropTypes.string.isRequired,
225 | shortDescription: PropTypes.string,
226 | numberOfView: PropTypes.number,
227 | like: PropTypes.number,
228 | width: PropTypes.number.isRequired,
229 | height: PropTypes.number.isRequired,
230 | githubLink: PropTypes.string,
231 | owner: PropTypes.shape({ id: PropTypes.string }).isRequired,
232 | category: PropTypes.arrayOf(PropTypes.string),
233 | stars: PropTypes.number,
234 | user: PropTypes.shape({ nickname: PropTypes.string }),
235 | checked: PropTypes.bool,
236 | rotate: PropTypes.bool,
237 | type: PropTypes.string,
238 | action: PropTypes.string,
239 | memberCount: PropTypes.string,
240 | job: PropTypes.shape({ title: PropTypes.string, siteUrl: PropTypes.string })
241 | .isRequired
242 | }
243 |
244 | AppDetail.defaultProps = {
245 | category: pkg.keywords,
246 | numberOfView: 0,
247 | like: 0,
248 | shortDescription: pkg.description,
249 | githubLink: undefined,
250 | stars: 0,
251 | user: undefined,
252 | checked: false,
253 | rotate: false,
254 | type: 'developer',
255 | action: process.env.MAILCHIMP_ACTION,
256 | memberCount: process.env.MAILCHIMP_MEMBER_COUNT_DEFAULT
257 | }
258 |
259 | AppDetail.getInitialProps = async ({ req, query }) => {
260 | const { slug, username } = query
261 | const gif = await getGifBySlugAsync(req, slug)
262 | const { width, height } = await getGifInfo(gif.id)
263 | await putIncrementNumberOfViewAsync(req, gif.id)
264 | const stars = gif.githubLink
265 | ? await getStargazersCountAsync(getFullNameFormUrl(gif.githubLink))
266 | : 0
267 | const user = process.browser
268 | ? getUserFromLocalCookie()
269 | : getUserFromServerCookie(req)
270 | const likes = user ? await getUserLikesAsync(req, user && user.nickname) : []
271 | const memberCount = await memberCountAsync(req)
272 | const jobs = await getJobsAsync()
273 | return {
274 | ...gif,
275 | username,
276 | originalUrl: req.originalUrl,
277 | width,
278 | height,
279 | stars,
280 | memberCount,
281 | checked: (likes && likes.includes(gif.id)) || false,
282 | job: jobs[Math.round(Math.random() * (jobs.length - 1))]
283 | }
284 | }
285 |
286 | export default defaultPage(AppDetail)
287 |
--------------------------------------------------------------------------------