├── .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 | <h1 5 | className="Title-s9pqsj-0 fFCGq" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/Pulse.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Pulse /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Pulse-s1pfiyxk-0 fABGZs" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/CleanHr.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<CleanHr /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="CleanHr-u75eti-0 fnhkPK" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/FadeIn.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<FadeIn /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="FadeIn-s1muu1d1-0 jOnkYB" 6 | /> 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[`<Hideable /> toMatchSnapshot 1`] = ` 4 | <span 5 | className="Hideable-lb4mgy-0 chPphA" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/BounceInUp.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<BounceInUp /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="BounceInUp-s7n96d-0 lazCtI" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/SlideInUp.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<SlideInUp /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="SlideInUp-s178605u-0 dTWHXn" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/Paragraph.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Paragraph /> toMatchSnapshot 1`] = ` 4 | <p 5 | className="Paragraph__Subtitle-s1secd56-0 iVQIYc" 6 | /> 7 | `; 8 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/VerticalyCentered.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<VerticalyCentered /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="VerticalyCentered-s2kwuh3-0 fEVrzi" 6 | /> 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 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> 2 | <url> 3 | <loc>https://reactnative.gallery/</loc> 4 | </url> 5 | <url> 6 | <loc>https://reactnative.gallery/upload/</loc> 7 | </url> 8 | <url> 9 | <loc>https://reactnative.gallery/about/</loc> 10 | </url> 11 | </urlset> -------------------------------------------------------------------------------- /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[`<Play /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Play-s10ikt1c-0 dcOaWh" 6 | /> 7 | `; 8 | 9 | exports[`<Play /> toMatchSnapshot 2 1`] = ` 10 | <div 11 | className="Play-s10ikt1c-0 gLlmwW" 12 | /> 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 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-video"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg> -------------------------------------------------------------------------------- /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[`<Subtitle /> toMatchSnapshot 1`] = ` 4 | <h3 5 | className="Subtitle-s1pqjucr-0 kyHEyY" 6 | /> 7 | `; 8 | 9 | exports[`<Subtitle /> toMatchSnapshot 2 1`] = ` 10 | <h3 11 | className="Subtitle-s1pqjucr-0 hQpFcq" 12 | /> 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[`<Smartphone /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Smartphone-s165niw0-0 bQCxFY" 6 | /> 7 | `; 8 | 9 | exports[`<Smartphone /> toMatchSnapshot 2 1`] = ` 10 | <div 11 | className="Smartphone-s165niw0-0 eEOMIr" 12 | /> 13 | `; 14 | -------------------------------------------------------------------------------- /static/images/play-circle.svg: -------------------------------------------------------------------------------- 1 | <svg 2 | xmlns="http://www.w3.org/2000/svg" 3 | width="24" 4 | height="24" 5 | viewBox="0 0 24 24" 6 | fill="black" 7 | stroke="white" 8 | stroke-width="2" 9 | stroke-linecap="round" 10 | stroke-linejoin="round" 11 | > 12 | <circle cx="12" cy="12" r="10"/> 13 | <polygon fill="white" points="10 8 16 12 10 16 10 8"/> 14 | </svg> -------------------------------------------------------------------------------- /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(<Love />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Love /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Love />).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(<Pulse />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Pulse /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Pulse />).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(<Title />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Title /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Title />).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(<FadeIn />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<FadeIn /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<FadeIn />).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(<Notice />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Notice /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Notice />).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(<CleanHr />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<CleanHr /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<CleanHr />).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(<Comment />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Comment /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Comment />).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(<Footer />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | // it('<Footer /> toMatchSnapshot', () => { 11 | // const tree = renderer.create(<Footer />).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(<Hideable />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Hideable /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Hideable />).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(<ViewIcon />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<ViewIcon /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<ViewIcon />).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(<Paragraph />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Paragraph /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Paragraph />).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(<SlideInUp />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<SlideInUp /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<SlideInUp />).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(<BounceInUp />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<BounceInUp /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<BounceInUp />).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(<GithubRibbon />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<GithubRibbon /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<GithubRibbon />).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 }) => <JobAdd>{title}</JobAdd> 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 | <Icon> 8 | {renderSmallIcon(Eye)} 9 | <Icon.Label>{number}</Icon.Label> 10 | </Icon> 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(<VerticalyCentered />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<VerticalyCentered /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<VerticalyCentered />).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[`<GithubRibbon /> toMatchSnapshot 1`] = ` 4 | <a 5 | href="http://github.com/ReactNativeGallery/reactnative-gallery-web" 6 | rel="noopener noreferrer" 7 | target="_blank" 8 | > 9 | <img 10 | alt="Fork me on GitHub" 11 | className="GithubRibbon__Ribbon-s1o3krv1-0 gNdiFL" 12 | src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" 13 | /> 14 | </a> 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 | <Icon> 8 | {renderSmallIcon(MessageCircle)} 9 | <Icon.Label>{number}</Icon.Label> 10 | </Icon> 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 | <Container> 10 | <Users size={48} /> 11 | <Speech> 12 | <Paragraph>Coming soon!</Paragraph> 13 | </Speech> 14 | </Container> 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 <VerticalyCentered id={CONTAINER_ID} /> 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(<Play />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Play /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Play />).toJSON() 12 | expect(tree).toMatchSnapshot() 13 | }) 14 | 15 | it('<Play /> toMatchSnapshot 2', () => { 16 | const tree = renderer.create(<Play show />).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 | <Icon pointer onClick={() => window.open(link)}> 8 | {renderSmallIcon(Github)} 9 | <Icon.Label>{number}</Icon.Label> 10 | </Icon> 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(<Subtitle />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Subtitle /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Subtitle />).toJSON() 12 | expect(tree).toMatchSnapshot() 13 | }) 14 | 15 | it('<Subtitle /> toMatchSnapshot 2', () => { 16 | const tree = renderer.create(<Subtitle hidexs />).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(<Smartphone />) 7 | expect(comp).toBeDefined() 8 | }) 9 | 10 | it('<Smartphone /> toMatchSnapshot', () => { 11 | const tree = renderer.create(<Smartphone />).toJSON() 12 | expect(tree).toMatchSnapshot() 13 | }) 14 | 15 | it('<Smartphone /> toMatchSnapshot 2', () => { 16 | const tree = renderer.create(<Smartphone cursorPointer />).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 | <Icon pointer onClick={onClick}> 8 | {renderSmallIcon(Heart, { fill: checked ? '#fff' : 'none' })} 9 | <Icon.Label>{number}</Icon.Label> 10 | </Icon> 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 | <svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns"> 2 | 3 | <g id="Canvas" transform="translate(-2639 -1639)" figma:type="canvas"> 4 | <g id="mark" style="mix-blend-mode:normal;" figma:type="vector"> 5 | <use xlink:href="#path0_fill" transform="translate(2639 1639)" fill="#ffffff" style="mix-blend-mode:normal;"/> 6 | </g> 7 | </g> 8 | <defs> 9 | <path id="path0_fill" fill-rule="evenodd" d="M 0 0L 3 0C 14.598 0 24 9.40202 24 21L 24 24L 13 24C 12.4477 24 12 23.5523 12 23L 12 21C 12 16.0294 7.97056 12 3 12L 1 12C 0.447715 12 0 11.5523 0 11L 0 0Z"/> 10 | </defs> 11 | </svg> -------------------------------------------------------------------------------- /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[`<ViewIcon /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Icon-s7tm2sn-0 cXvtWu" 6 | > 7 | <svg 8 | fill="none" 9 | height={22} 10 | stroke="#fff" 11 | strokeLinecap="round" 12 | strokeLinejoin="round" 13 | strokeWidth="2" 14 | viewBox="0 0 24 24" 15 | width={22} 16 | xmlns="http://www.w3.org/2000/svg" 17 | > 18 | <path 19 | d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" 20 | /> 21 | <circle 22 | cx="12" 23 | cy="12" 24 | r="3" 25 | /> 26 | </svg> 27 | <span 28 | className="Icon__Label-s7tm2sn-1 grNILv" 29 | > 30 | 0 31 | </span> 32 | </div> 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[`<Love /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Icon-s7tm2sn-0 jglmtW" 6 | onClick={[Function]} 7 | > 8 | <svg 9 | fill="none" 10 | height={22} 11 | stroke="#fff" 12 | strokeLinecap="round" 13 | strokeLinejoin="round" 14 | strokeWidth="2" 15 | viewBox="0 0 24 24" 16 | width={22} 17 | xmlns="http://www.w3.org/2000/svg" 18 | > 19 | <path 20 | d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" 21 | /> 22 | </svg> 23 | <span 24 | className="Icon__Label-s7tm2sn-1 grNILv" 25 | > 26 | 0 27 | </span> 28 | </div> 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 <div /> 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[`<Comment /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Icon-s7tm2sn-0 cXvtWu" 6 | > 7 | <svg 8 | fill="none" 9 | height={22} 10 | stroke="#fff" 11 | strokeLinecap="round" 12 | strokeLinejoin="round" 13 | strokeWidth="2" 14 | viewBox="0 0 24 24" 15 | width={22} 16 | xmlns="http://www.w3.org/2000/svg" 17 | > 18 | <path 19 | d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" 20 | /> 21 | </svg> 22 | <span 23 | className="Icon__Label-s7tm2sn-1 grNILv" 24 | > 25 | 0 26 | </span> 27 | </div> 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 | <Comp style={{ marginBottom: -2, marginRight: 3.5 }} size="22" /> 22 | ) 23 | 24 | export const renderSmallIcon = (Comp, props) => ( 25 | <Comp color="#fff" size={22} {...props} /> 26 | ) 27 | 28 | export const Next = () => ( 29 | <ChevronRight size="50" color="#777" style={{ marginBottom: -20 }} /> 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 | <a 19 | href="http://github.com/ReactNativeGallery/reactnative-gallery-web" 20 | target="_blank" 21 | rel="noopener noreferrer" 22 | > 23 | <Ribbon src={src} alt={alt} /> 24 | </a> 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[`<Notice /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Notice__NoticeContainer-s1hmz53f-0 lBPtN" 6 | > 7 | <svg 8 | fill="none" 9 | height={17} 10 | stroke="currentColor" 11 | strokeLinecap="round" 12 | strokeLinejoin="round" 13 | strokeWidth="2" 14 | viewBox="0 0 24 24" 15 | width={17} 16 | xmlns="http://www.w3.org/2000/svg" 17 | > 18 | <circle 19 | cx="12" 20 | cy="12" 21 | r="10" 22 | /> 23 | <line 24 | x1="12" 25 | x2="12" 26 | y1="16" 27 | y2="12" 28 | /> 29 | <line 30 | x1="12" 31 | x2="12" 32 | y1="8" 33 | y2="8" 34 | /> 35 | </svg> 36 | <small 37 | className="Notice__NoticeText-s1hmz53f-1 gBfhZM" 38 | > 39 | Hover to play app video 40 | </small> 41 | </div> 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 | <Wrapper> 7 | <p>test</p> 8 | </Wrapper> 9 | ) 10 | 11 | // const compToTest2 = <Wrapper /> 12 | 13 | // const compToTest3 = <Wrapper footer /> 14 | 15 | it('Wrapper can be created', () => { 16 | const comp = renderer.create(compToTest) 17 | expect(comp).toBeDefined() 18 | }) 19 | 20 | // it('<Wrapper /> toMatchSnapshot', () => { 21 | // const tree = renderer.create(compToTest).toJSON() 22 | // expect(tree).toMatchSnapshot() 23 | // }) 24 | 25 | // it('<Wrapper /> toMatchSnapshot', () => { 26 | // const tree = renderer.create(compToTest2).toJSON() 27 | // expect(tree).toMatchSnapshot() 28 | // }) 29 | 30 | // it('<Wrapper /> 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 | <NoticeContainer> 35 | <Info size={17} /> 36 | <NoticeText>Hover to play app video</NoticeText> 37 | </NoticeContainer> 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(<Gif gifId="test" username="x" slug="s" />) 11 | expect(comp).toBeDefined() 12 | }) 13 | 14 | it('<Gif /> toMatchSnapshot', () => { 15 | const tree = renderer 16 | .create(<Gif gifId="test" username="xcarpentier" slug="slug" />) 17 | .toJSON() 18 | expect(tree).toMatchSnapshot() 19 | }) 20 | 21 | it('<Gif /> toMatchSnapshot click', () => { 22 | const gif = shallow(<Gif gifId="test" username="xcarpentier" slug="slug" />) 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 | <Fragment> 31 | <Wrapper>{children}</Wrapper> 32 | {footer && <Footer />} 33 | </Fragment> 34 | ) 35 | } 36 | 37 | Wrap.defaultProps = { 38 | footer: false, 39 | children: <p style={{ color: '#f55' }}>Nothing to render</p> 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 | <div onClick={this.goToDetail}> 24 | <Gif gifId={id} autoplay styles={{ margin: '20px auto' }} /> 25 | </div> 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 | <Wrapper footer> 40 | <Page {...this.props} /> 41 | </Wrapper> 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(<MailchimpForm 11 | action="test" 12 | memberCount="1" 13 | type="test" 14 | email="test@email.com" 15 | onChange={() => {}} 16 | />) 17 | expect(comp).toBeDefined() 18 | }) 19 | 20 | it('<MailchimpForm /> toMatchSnapshot', () => { 21 | const tree = renderer 22 | .create(<MailchimpForm 23 | action="test" 24 | memberCount="1" 25 | type="test" 26 | email="test@email.com" 27 | onChange={() => {}} 28 | />) 29 | .toJSON() 30 | expect(tree).toMatchSnapshot() 31 | }) 32 | 33 | it('<MailchimpForm /> simulate input change ', () => { 34 | // TODO: find a way to pass into onChange 35 | const email = 'cool@gmel.fr' 36 | const form = shallow(<MailchimpForm 37 | action="test" 38 | memberCount="1" 39 | type="test" 40 | email={email} 41 | onChange={() => {}} 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[`<Gif /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="Smartphone-s165niw0-0 bQCxFY" 6 | onClick={[Function]} 7 | onMouseEnter={[Function]} 8 | onMouseLeave={[Function]} 9 | rotate={undefined} 10 | style={ 11 | Object { 12 | "background": false, 13 | } 14 | } 15 | > 16 | <div 17 | className="GifContainer-s1wbubqp-0 fvwRPH" 18 | > 19 | <div 20 | className="Play-s10ikt1c-0 gLlmwW" 21 | /> 22 | <div 23 | className="Gif__Sizer-s1j91rz1-0 gpJmvZ" 24 | /> 25 | <video 26 | autoPlay={false} 27 | loop={true} 28 | muted={true} 29 | playsInline={true} 30 | poster="https://thumbs.gfycat.com/test-poster.jpg" 31 | preload="none" 32 | style={ 33 | Object { 34 | "height": "100%", 35 | "left": 0, 36 | "position": "absolute", 37 | "top": 0, 38 | "transform": "none", 39 | "width": "100%", 40 | } 41 | } 42 | > 43 | <track 44 | kind="captions" 45 | /> 46 | <source 47 | src="https://giant.gfycat.com/test.webm" 48 | type="video/webm" 49 | /> 50 | <source 51 | src="https://giant.gfycat.com/test.mp4" 52 | type="video/mp4" 53 | /> 54 | </video> 55 | </div> 56 | <div 57 | className="Button__ButtonContainer-s1yo1pni-0 kZcYqm" 58 | > 59 | <div 60 | alt="Show detail" 61 | className="Button-s1yo1pni-1 ewehdc" 62 | onClick={[Function]} 63 | onFocus={[Function]} 64 | onMouseLeave={[Function]} 65 | onMouseOver={[Function]} 66 | /> 67 | </div> 68 | </div> 69 | `; 70 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/MailchimpForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<MailchimpForm /> toMatchSnapshot 1`] = ` 4 | <div 5 | className="MailchimpForm__Container-s1jd0qpd-0 loDsvJ" 6 | > 7 | <div 8 | className="CleanHr-u75eti-0 fnhkPK" 9 | /> 10 | <form 11 | action="test" 12 | className="MailchimpForm-s1jd0qpd-1 fHspRs" 13 | method="POST" 14 | name="form" 15 | noValidate="" 16 | target="_blank" 17 | > 18 | <input 19 | className="MailchimpForm__MailchimpInput-s1jd0qpd-3 boglBc" 20 | name="EMAIL" 21 | onChange={[Function]} 22 | placeholder="Enter email" 23 | required="required" 24 | type="email" 25 | value="test@email.com" 26 | /> 27 | <button 28 | className="MailchimpForm__MailchimpButton-s1jd0qpd-4 jIwtxb" 29 | type="submit" 30 | > 31 | <span 32 | className="Hideable-lb4mgy-0 bDYHzV" 33 | > 34 | <strong> 35 | JOIN 36 | </strong> 37 | </span> 38 | <span 39 | className="Hideable-lb4mgy-0 UfVDy" 40 | > 41 | <svg 42 | fill="none" 43 | height="24" 44 | stroke="currentColor" 45 | strokeLinecap="round" 46 | strokeLinejoin="round" 47 | strokeWidth="2" 48 | viewBox="0 0 24 24" 49 | width="24" 50 | xmlns="http://www.w3.org/2000/svg" 51 | > 52 | <polyline 53 | points="20 6 9 17 4 12" 54 | /> 55 | </svg> 56 | </span> 57 | </button> 58 | <input 59 | name="TYPE" 60 | type="hidden" 61 | value="test" 62 | /> 63 | </form> 64 | <small 65 | className="MailchimpForm__SmallLabel-s1jd0qpd-2 fFWHHo" 66 | > 67 | Join 68 | <strong> 69 | 1 70 | </strong> 71 | members 72 | </small> 73 | </div> 74 | `; 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awesome [reactnative.gallery](https://reactnative.gallery) website 2 | 3 | <p align="center"> 4 | <a href="https://reactnative.gallery"> 5 | <img alt="reactnative.gallery" src="https://raw.githubusercontent.com/ReactNativeGallery/reactnative-gallery-web/master/static/images/background.jpeg"/> 6 | </a> 7 | </p> 8 | 9 | 👉 [![reactnative.gallery](https://img.shields.io/badge/reactnative.gallery-%F0%9F%8E%AC-green.svg)](https://reactnative.gallery) 👈 10 | [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/reactnative-gallery) 11 | [![style: styled-components](https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg?colorB=daa357&colorA=db748e)](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 | <Container> 21 | <Info size={48} /> 22 | <Speech> 23 | <Logo src="/static/images/logo.png" /> 24 | <Paragraph> 25 | <strong>Reactnative.gallery</strong> is a website where you can 26 | visualize apps and open source components as videos.<br /> 27 | <br />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 | </Paragraph> 32 | <Paragraph> 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 | </Paragraph> 36 | <Paragraph> 37 | <b>Reactnative.gallery</b> makes it possible to not only{' '} 38 | <strong>visualize apps at a glance</strong> using videos, but also to 39 | describe the app, categorize it, do a search and above all{' '} 40 | <strong>share it with the rest of the community</strong>. 41 | </Paragraph> 42 | <Paragraph> 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 | </Paragraph> 47 | <Paragraph> 48 | For open-source developers, you can login with GitHub and your animated 49 | gifs will be <strong>automatically recognized and shared</strong>, and 50 | then can receive feedback from the community (comments and the number of 51 | views and likes are displayed). 52 | </Paragraph> 53 | <Paragraph> 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 | </Paragraph> 58 | </Speech> 59 | </Container> 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(<App {...props} />)) 20 | const styleTags = sheet.getStyleElement() 21 | return { ...page, styleTags } 22 | } 23 | 24 | render() { 25 | return ( 26 | <html lang="en" prefix="og: http://ogp.me/ns#"> 27 | <Head> 28 | <title>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 |