├── .babelrc ├── .eslintignore ├── .gitignore ├── .gitmodules ├── .storybook ├── .babelrc ├── config.js └── webpack.config.js ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .yarnclean ├── README.md ├── app ├── _tests │ ├── __snapshots__ │ │ └── storyshots.test.js.snap │ └── storyshots.test.ts ├── components │ └── artist │ │ ├── _stories │ │ └── _header.story.tsx │ │ ├── _tests │ │ └── header.test.ts │ │ └── header.tsx ├── containers │ ├── pure-react │ │ └── artist │ │ │ ├── artwork.tsx │ │ │ ├── browser.tsx │ │ │ ├── grid.tsx │ │ │ ├── index.tsx │ │ │ └── style.css │ ├── react-aphrodite │ │ └── artist │ │ │ ├── artwork.tsx │ │ │ ├── browser.tsx │ │ │ ├── grid.tsx │ │ │ └── index.tsx │ ├── react-inline-css │ │ └── artist │ │ │ ├── artwork.tsx │ │ │ ├── browser.tsx │ │ │ ├── grid.tsx │ │ │ └── index.tsx │ ├── react-jss │ │ └── artist │ │ │ ├── artwork.tsx │ │ │ ├── browser.tsx │ │ │ ├── grid.tsx │ │ │ └── index.tsx │ └── react-native-web │ │ └── artist │ │ ├── artwork.tsx │ │ ├── browser.tsx │ │ ├── grid.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ ├── inverted_button.tsx │ │ ├── metaphysics.ts │ │ ├── switch_board.ts │ │ └── text │ │ ├── headline.tsx │ │ └── serif.tsx ├── gql.d.ts ├── relay │ ├── config.ts │ └── root_queries.ts └── routes.tsx ├── data ├── colors.json ├── schema.graphql ├── schema.js └── schema.json ├── index.js ├── jsconfig.json ├── package.json ├── tsconfig.json ├── tslint.json ├── types ├── global.d.ts ├── react-native-web.d.ts └── react-native.d.ts ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-3", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "./data/schema", 9 | "transform-flow-strip-types", 10 | "syntax-async-functions", 11 | "transform-regenerator", 12 | "transform-class-properties", 13 | ["module-resolver", { "alias": { "react-native": "react-native-web" } }] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | externals 3 | data 4 | flow-typed 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | public 4 | out 5 | .awcache 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "externals/metaphysics"] 2 | path = externals/metaphysics 3 | url = https://github.com/artsy/metaphysics.git 4 | [submodule "externals/elan"] 5 | path = externals/elan 6 | url = https://github.com/artsy/elan.git 7 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | // This needs to make sure that the relative path of the 3 | // relay config babel extension is set to the right path. 4 | "extends": "../.babelrc" 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@kadira/storybook' 2 | 3 | // TODO: Add globbing: e.g. ../app/containers/*/_stories/*.story.js 4 | // could use micromatch, which is used in Jest 5 | 6 | function loadStories() { 7 | require('../app/containers/artist/_stories/_header.story') 8 | } 9 | 10 | configure(loadStories, module); 11 | 12 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | module: { 5 | loaders: [ 6 | { 7 | include: path.resolve(__dirname, "../") 8 | loaders: ["style", "css", "sass"], 9 | test: /.scss$/, 10 | }, 11 | ], 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for node debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Program", 9 | "type": "node2", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/index.js", 12 | "cwd": "${workspaceRoot}" 13 | }, 14 | { 15 | "name": "Attach to Process", 16 | "type": "node2", 17 | "request": "attach", 18 | "port": 9229 19 | }, 20 | { 21 | "name": "Run Tests With Debugger (slower, use npm run watch for normal work)", 22 | "type": "node2", 23 | "request": "launch", 24 | "port": 5858, 25 | "address": "localhost", 26 | "sourceMaps": true, 27 | "stopOnEntry": false, 28 | "runtimeExecutable": null, 29 | "runtimeArgs": [ 30 | "--debug-brk", 31 | "./node_modules/.bin/jest", 32 | "-i" 33 | ], 34 | "cwd": "${workspaceRoot}" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "javascript.validate.enable": false, 4 | "tslint.run": "onType", 5 | "tslint.autoFixOnSave": true, 6 | "editor.formatOnType": true, 7 | "editor.formatOnSave": true 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "yarn", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } 31 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | tests 4 | powered-test 5 | 6 | # asset directories 7 | docs 8 | doc 9 | website 10 | images 11 | assets 12 | 13 | # examples 14 | example 15 | examples 16 | 17 | # code coverage directories 18 | coverage 19 | .nyc_output 20 | 21 | # build scripts 22 | Makefile 23 | Gulpfile.js 24 | Gruntfile.js 25 | 26 | # configs 27 | .tern-project 28 | .gitattributes 29 | .editorconfig 30 | .*ignore 31 | .eslintrc 32 | .jshintrc 33 | .flowconfig 34 | .documentup.json 35 | .yarn-metadata.json 36 | .*.yml 37 | *.yml 38 | 39 | # misc 40 | *.gz 41 | *.md 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | These are some experiments with using React and Relay for both server and client side rendering. 4 | 5 | ## Setup 6 | 7 | ``` 8 | $ npm install yarn -g 9 | $ yarn install 10 | ``` 11 | 12 | Then start the development server with: 13 | 14 | ``` 15 | $ yarn start 16 | ``` 17 | 18 | To run storybooks: 19 | 20 | ``` 21 | $ yarn run storybooks 22 | ``` 23 | 24 | ## Development environment 25 | 26 | * Rather than restart the full server process, which is often done with tools like `nodemon`, this project only reloads 27 | the files in `./app` when FS changes occur. 28 | 29 | * In addition, webpack is configured to dynamically host client sources, meaning that these will also automatically be 30 | recompiled when FS changes occur. 31 | 32 | * Finally, webpack is configured to notify the client of changes to sources which then fetches the updated sources and 33 | triggers a re-render of the React components that are on screen. 34 | 35 | This setup is largely based on https://github.com/glenjamin/ultimate-hot-reloading-example. 36 | 37 | ## TODO 38 | 39 | * Take a look at react-native-web, how well it works, and if it would make sharing our components across platforms 40 | possible out of the box. 41 | 42 | ## Notes 43 | 44 | * In order for `react-hot-loader` to be able to reload components and maintain their current state, rather than a page 45 | reload, the components have to be exported themselves, not just the Relay wrapper container 46 | ([more info](https://github.com/fortruce/relay-skeleton/issues/1)). E.g. 47 | 48 | ```js 49 | export class Artist extends React.component { 50 | ... 51 | } 52 | 53 | export default Relay.createContainer(Artist, { 54 | ... 55 | }) 56 | ``` 57 | 58 | ## Examples 59 | 60 | ### Hot Loader in Action 61 | 62 | ![hot loader in action](https://cloud.githubusercontent.com/assets/2320/21506765/b89930a6-cc40-11e6-8a80-7b167de6edb4.gif) 63 | 64 | ### Hot Loader Syntax Error Overlay 65 | 66 | ![hot loader syntax error](https://cloud.githubusercontent.com/assets/2320/21506725/7545e09c-cc40-11e6-9098-6d0fe6e4b747.png) 67 | 68 | -------------------------------------------------------------------------------- /app/_tests/__snapshots__/storyshots.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Storyshots Artist Header For Stubbed Data 1`] = ` 2 |
3 |

4 | Another Example 5 |

6 |
7 | 8 | OK, b. 1999 9 | 10 |
11 | 12 | 12 Followers 13 | 14 |
15 | `; 16 | -------------------------------------------------------------------------------- /app/_tests/storyshots.test.ts: -------------------------------------------------------------------------------- 1 | import initStoryshots from "storyshots" 2 | 3 | initStoryshots({ 4 | storyRegex: /^((?!(r|R)elay).)*$/, 5 | }) 6 | -------------------------------------------------------------------------------- /app/components/artist/_stories/_header.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@kadira/storybook" 2 | import * as React from "react" 3 | import * as Relay from "react-relay" 4 | import StubContainer from "react-storybooks-relay-container" 5 | 6 | import Header from "../header" 7 | 8 | import { artsyNetworkLayer } from "../../../relay/config" 9 | import { ArtistQueryConfig } from "../../../relay/root_queries" 10 | 11 | storiesOf("Artist Header", module) 12 | .add("Relay : Artist - Leda Catunda", () => { 13 | Relay.injectNetworkLayer(artsyNetworkLayer()) 14 | const artistRoute = new ArtistQueryConfig({ artistID: "leda-catunda" }) 15 | return 16 | }) 17 | 18 | .add("For Stubbed Data", () => { 19 | const api = { 20 | artist: { 21 | birthday: "1999", 22 | counts: { follows: 12 }, 23 | name: "Another Example", 24 | nationality: "OK", 25 | }, 26 | } 27 | return 28 | }) 29 | -------------------------------------------------------------------------------- /app/components/artist/_tests/header.test.ts: -------------------------------------------------------------------------------- 1 | describe("test", () => { 2 | it("tests", () => { 3 | expect(1).toEqual(1) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /app/components/artist/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | import GQL from "../../gql" 4 | 5 | interface Props { 6 | artist: GQL.ArtistType 7 | } 8 | 9 | interface State { 10 | following: boolean | null, 11 | followersCount: number, 12 | } 13 | 14 | class Header extends React.Component { 15 | 16 | constructor(props: any) { 17 | super(props) 18 | this.state = { following: null, followersCount: props.artist.counts.follows } 19 | } 20 | 21 | render() { 22 | const artist = this.props.artist 23 | return ( 24 |
25 |

{artist.name}

26 | {this.renderByline()} 27 | {this.renderFollowersCount()} 28 |
29 | ) 30 | } 31 | 32 | renderFollowersCount() { 33 | const count = this.state.followersCount 34 | const followerString = count + (count === 1 ? " Follower" : " Followers") 35 | return ( 36 | {followerString} 37 | ) 38 | } 39 | 40 | renderByline() { 41 | const artist = this.props.artist 42 | const bylineRequired = (artist.nationality || artist.birthday) 43 | if (bylineRequired) { 44 | return ( 45 |
46 | {this.descriptiveString()} 47 |
48 | ) 49 | } else { 50 | return null 51 | } 52 | } 53 | 54 | descriptiveString() { 55 | const artist = this.props.artist 56 | const descriptiveString = (artist.nationality || "") + this.birthdayString() 57 | return descriptiveString 58 | } 59 | 60 | birthdayString() { 61 | const birthday = this.props.artist.birthday 62 | if (!birthday) { return "" } 63 | 64 | const leadingSubstring = this.props.artist.nationality ? ", b." : "" 65 | 66 | if (birthday.includes("born")) { 67 | return birthday.replace("born", leadingSubstring) 68 | } else if (birthday.includes("Est.") || birthday.includes("Founded")) { 69 | return " " + birthday 70 | } 71 | 72 | return leadingSubstring + " " + birthday 73 | } 74 | } 75 | 76 | // TODO: Orta - what's up with this? 77 | export default Relay.createContainer(Header as any, { 78 | fragments: { 79 | artist: () => Relay.QL` 80 | fragment on Artist { 81 | _id 82 | id 83 | name 84 | nationality 85 | birthday 86 | counts { 87 | follows 88 | } 89 | } 90 | `, 91 | }, 92 | }) 93 | -------------------------------------------------------------------------------- /app/containers/pure-react/artist/artwork.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | interface Props { 5 | artwork: any 6 | } 7 | interface State { 8 | } 9 | 10 | export class Artwork extends React.Component { 11 | render() { 12 | return ( 13 |
14 | 15 |

{this.props.artwork.artists.map((artist) => artist.name).join(", ")}

16 |

{this.props.artwork.title}

17 |
18 | ) 19 | } 20 | } 21 | 22 | export default Relay.createContainer(Artwork, { 23 | fragments: { 24 | artwork: () => Relay.QL` 25 | fragment on Artwork { 26 | title 27 | artists { 28 | name 29 | } 30 | image { 31 | url 32 | } 33 | } 34 | `, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /app/containers/pure-react/artist/browser.tsx: -------------------------------------------------------------------------------- 1 | import IsomorphicRelay from "isomorphic-relay" 2 | import * as React from "react" 3 | import * as ReactDOM from "react-dom" 4 | 5 | import { artsyRelayEnvironment } from "../../../relay/config" 6 | import { ArtistQueryConfig } from "../../../relay/root_queries" 7 | 8 | declare var document: any 9 | declare var window: any 10 | 11 | import Artist from "./index" 12 | 13 | const rootElement = document.getElementById("root") 14 | 15 | const environment = artsyRelayEnvironment() 16 | IsomorphicRelay.injectPreparedData(environment, window.ARTIST_PROPS) 17 | 18 | IsomorphicRelay.prepareInitialRender({ 19 | Container: Artist, 20 | queryConfig: new ArtistQueryConfig({ artistID: window.ARTIST_ID }), 21 | environment, 22 | }).then((props) => { 23 | ReactDOM.render(, rootElement) 24 | }) 25 | -------------------------------------------------------------------------------- /app/containers/pure-react/artist/grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import Artwork from "./artwork" 5 | 6 | interface Props { 7 | sectionMargin: number, 8 | itemMargin: number, 9 | artworks: any[] 10 | } 11 | interface State { 12 | sectionCount: number, 13 | } 14 | 15 | export class Grid extends React.Component { 16 | // state: { 17 | // sectionDimension: number, 18 | // sectionCount: number, 19 | // }; 20 | 21 | static defaultProps = { 22 | // sectionDirection: 'column', 23 | itemMargin: 20, 24 | sectionMargin: 20, 25 | } 26 | 27 | constructor(props) { 28 | super(props) 29 | this.state = { 30 | // sectionDimension: 0, 31 | sectionCount: 3, 32 | } 33 | 34 | // this.onLayout = this.onLayout.bind(this) 35 | } 36 | 37 | // layoutState(currentLayout) : Object { 38 | // const width = currentLayout.width 39 | // const isPad = width > 600 40 | // const isPadHorizontal = width > 900 41 | 42 | // const sectionCount = isPad ? (isPadHorizontal ? 4 : 3) : 2 43 | // const sectionMargins = this.props.sectionMargin * (sectionCount - 1) 44 | // const sectionDimension = (currentLayout.width - sectionMargins) / sectionCount 45 | 46 | // return { sectionCount: sectionCount, 47 | // sectionDimension: sectionDimension, 48 | // } 49 | // } 50 | 51 | // onLayout = (event: LayoutEvent) => { 52 | // const layout = event.nativeEvent.layout 53 | // const newLayoutState = this.layoutState(layout) 54 | // if (layout.width > 0) { 55 | // this.setState(newLayoutState) 56 | // } 57 | // } 58 | 59 | sectionedArtworks() { 60 | const sectionedArtworks = [] 61 | const sectionRatioSums = [] 62 | for (let i = 0; i < this.state.sectionCount; i++) { 63 | sectionedArtworks.push([]) 64 | sectionRatioSums.push(0) 65 | } 66 | 67 | const artworks = this.props.artworks 68 | for (const artwork of artworks) { 69 | if (artwork.image) { 70 | let lowestRatioSum = Number.MAX_VALUE 71 | let sectionIndex: number | null = null 72 | 73 | for (let j = 0; j < sectionRatioSums.length; j++) { 74 | const ratioSum = sectionRatioSums[j] 75 | if (ratioSum < lowestRatioSum) { 76 | sectionIndex = j 77 | lowestRatioSum = ratioSum 78 | } 79 | } 80 | 81 | if (sectionIndex != null) { 82 | const section = sectionedArtworks[sectionIndex] 83 | section.push(artwork) 84 | 85 | // total section aspect ratio 86 | const aspectRatio = artwork.image.aspect_ratio || 1 87 | sectionRatioSums[sectionIndex] += (1 / aspectRatio) 88 | } 89 | } 90 | } 91 | return sectionedArtworks 92 | } 93 | 94 | renderSections() { 95 | // const spacerStyle = { 96 | // height: this.props.itemMargin, 97 | // } 98 | const sectionedArtworks = this.sectionedArtworks() 99 | const sections = [] 100 | for (let i = 0; i < this.state.sectionCount; i++) { 101 | const artworkComponents = [] 102 | const artworks = sectionedArtworks[i] 103 | for (let j = 0; j < artworks.length; j++) { 104 | const artwork = artworks[j] 105 | artworkComponents.push() 106 | if (j < artworks.length - 1) { 107 | // artworkComponents.push(
) 108 | artworkComponents.push(
) 109 | } 110 | } 111 | 112 | // const sectionSpecificStlye = { 113 | // width: this.state.sectionDimension, 114 | // marginRight: (i === this.state.sectionCount - 1 ? 0 : this.props.sectionMargin), 115 | // } 116 | sections.push( 117 | //
118 |
119 | {artworkComponents} 120 |
) 121 | } 122 | return sections 123 | } 124 | 125 | render() { 126 | return
{this.renderSections()}
127 | } 128 | } 129 | 130 | // const styles = StyleSheet.create({ 131 | // container: { 132 | // flexDirection: 'row', 133 | // }, 134 | // section: { 135 | // flexDirection: 'column', 136 | // }, 137 | // }) 138 | 139 | export default Relay.createContainer(Grid, { 140 | fragments: { 141 | artworks: () => Relay.QL` 142 | fragment on Artwork @relay(plural: true) { 143 | __id 144 | image { 145 | aspect_ratio 146 | } 147 | ${Artwork.getFragment("artwork")} 148 | } 149 | `, 150 | }, 151 | }) 152 | -------------------------------------------------------------------------------- /app/containers/pure-react/artist/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import ArtistHeader from "../../../components/artist/header" 5 | import Grid from "./grid" 6 | import GQL from "../../../gql" 7 | 8 | interface Props { 9 | artist: GQL.ArtistType, 10 | } 11 | interface State { 12 | following: boolean | null, 13 | followersCount: number, 14 | } 15 | 16 | export class Artist extends React.Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default Relay.createContainer(Artist, { 29 | fragments: { 30 | artist: () => Relay.QL` 31 | fragment on Artist { 32 | name 33 | counts { 34 | artworks, 35 | partner_shows 36 | articles 37 | } 38 | artworks(size: 30) { 39 | ${Grid.getFragment("artworks")} 40 | } 41 | ${ArtistHeader.getFragment("artist")} 42 | } 43 | `, 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /app/containers/pure-react/artist/style.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .grid .column { 7 | flex-direction: column; 8 | flex: 1 0 0px; 9 | max-width: 300px; 10 | margin-right: 20px; 11 | } 12 | 13 | .grid .column .artwork { 14 | flex: 1; 15 | margin-bottom: 20px; 16 | } 17 | 18 | .artwork img { 19 | width: 100%; 20 | } 21 | 22 | .artwork h3,h4 { 23 | margin: 4px 0; 24 | color: rgb(102, 102, 102); 25 | font-size: 15px; 26 | } 27 | 28 | .artwork h3 { 29 | font-style: normal; 30 | font-weight: bold; 31 | } 32 | 33 | .artwork h4 { 34 | font-style: italic; 35 | font-weight: normal 36 | } 37 | -------------------------------------------------------------------------------- /app/containers/react-aphrodite/artist/artwork.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import { css, StyleSheet } from "aphrodite/no-important" 5 | 6 | interface Props { 7 | artwork: any 8 | } 9 | interface State { 10 | } 11 | 12 | export class Artwork extends React.Component { 13 | render() { 14 | const artists = this.props.artwork.artists.map(artist => artist.name).join(", ") 15 | return ( 16 |
17 | 18 |

{artists}

19 |

{this.props.artwork.title}

20 |
21 | ) 22 | } 23 | } 24 | 25 | // .grid .column .artwork { 26 | // flex: 1; 27 | // margin-bottom: 20px; 28 | // } 29 | 30 | // .artwork img { 31 | // width: 100%; 32 | // } 33 | 34 | // .artwork h3,h4 { 35 | // margin: 4px 0; 36 | // color: rgb(102, 102, 102); 37 | // font-size: 15px; 38 | // } 39 | 40 | // .artwork h3 { 41 | // font-style: normal; 42 | // font-weight: bold; 43 | // } 44 | 45 | // .artwork h4 { 46 | // font-style: italic; 47 | // font-weight: normal 48 | // } 49 | 50 | const styles = StyleSheet.create({ 51 | artists: { 52 | fontStyle: "normal", 53 | fontWeight: "bold", 54 | }, 55 | container: { 56 | flex: 1, 57 | marginBottom: 20, 58 | }, 59 | heading: { 60 | color: "rgb(102, 102, 102)", 61 | fontSize: 15, 62 | margin: "4px 0", 63 | }, 64 | image: { 65 | width: "100%", 66 | }, 67 | title: { 68 | fontStyle: "italic", 69 | fontWeight: "normal", 70 | }, 71 | }) 72 | 73 | export default Relay.createContainer(Artwork, { 74 | fragments: { 75 | artwork: () => Relay.QL` 76 | fragment on Artwork { 77 | title 78 | artists { 79 | name 80 | } 81 | image { 82 | url 83 | } 84 | } 85 | `, 86 | }, 87 | }) 88 | -------------------------------------------------------------------------------- /app/containers/react-aphrodite/artist/browser.tsx: -------------------------------------------------------------------------------- 1 | import IsomorphicRelay from "isomorphic-relay" 2 | import React from "react" 3 | import ReactDOM from "react-dom" 4 | 5 | import { StyleSheet } from "aphrodite" 6 | 7 | import { artsyRelayEnvironment } from "../../../relay/config" 8 | import { ArtistQueryConfig } from "../../../relay/root_queries" 9 | 10 | import Artist from "./index" 11 | 12 | declare var document: any 13 | declare var window: any 14 | 15 | StyleSheet.rehydrate(window.STYLE_SHEET) 16 | 17 | const rootElement = document.getElementById("root") 18 | 19 | const environment = artsyRelayEnvironment() 20 | IsomorphicRelay.injectPreparedData(environment, window.ARTIST_PROPS) 21 | 22 | IsomorphicRelay.prepareInitialRender({ 23 | Container: Artist, 24 | queryConfig: new ArtistQueryConfig({ artistID: window.ARTIST_ID }), 25 | environment, 26 | }).then(props => { 27 | ReactDOM.render(, rootElement) 28 | }) 29 | -------------------------------------------------------------------------------- /app/containers/react-aphrodite/artist/grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import { css, StyleSheet } from "aphrodite/no-important" 5 | 6 | import Artwork from "./artwork" 7 | 8 | interface Props { 9 | sectionMargin: number, 10 | itemMargin: number, 11 | artworks: any[] 12 | } 13 | 14 | interface State { 15 | sectionCount: number, 16 | } 17 | 18 | export class Grid extends React.Component { 19 | // state: { 20 | // sectionDimension: number, 21 | // sectionCount: number, 22 | // }; 23 | 24 | static defaultProps = { 25 | // sectionDirection: "column", 26 | itemMargin: 20, 27 | sectionMargin: 20, 28 | } 29 | 30 | constructor(props) { 31 | super(props) 32 | this.state = { 33 | // sectionDimension: 0, 34 | sectionCount: 3, 35 | } 36 | 37 | // this.onLayout = this.onLayout.bind(this) 38 | } 39 | 40 | // layoutState(currentLayout) : Object { 41 | // const width = currentLayout.width 42 | // const isPad = width > 600 43 | // const isPadHorizontal = width > 900 44 | 45 | // const sectionCount = isPad ? (isPadHorizontal ? 4 : 3) : 2 46 | // const sectionMargins = this.props.sectionMargin * (sectionCount - 1) 47 | // const sectionDimension = (currentLayout.width - sectionMargins) / sectionCount 48 | 49 | // return { sectionCount: sectionCount, 50 | // sectionDimension: sectionDimension, 51 | // } 52 | // } 53 | 54 | // onLayout = (event: LayoutEvent) => { 55 | // const layout = event.nativeEvent.layout 56 | // const newLayoutState = this.layoutState(layout) 57 | // if (layout.width > 0) { 58 | // this.setState(newLayoutState) 59 | // } 60 | // } 61 | 62 | sectionedArtworks() { 63 | const sectionedArtworks = [] 64 | const sectionRatioSums = [] 65 | for (let i = 0; i < this.state.sectionCount; i++) { 66 | sectionedArtworks.push([]) 67 | sectionRatioSums.push(0) 68 | } 69 | 70 | const artworks = this.props.artworks 71 | for (const artwork of artworks) { 72 | 73 | if (artwork.image) { 74 | let lowestRatioSum = Number.MAX_VALUE 75 | let sectionIndex: number | null = null 76 | 77 | for (let j = 0; j < sectionRatioSums.length; j++) { 78 | const ratioSum = sectionRatioSums[j] 79 | if (ratioSum < lowestRatioSum) { 80 | sectionIndex = j 81 | lowestRatioSum = ratioSum 82 | } 83 | } 84 | 85 | if (sectionIndex != null) { 86 | const section = sectionedArtworks[sectionIndex] 87 | section.push(artwork) 88 | 89 | // total section aspect ratio 90 | const aspectRatio = artwork.image.aspect_ratio || 1 91 | sectionRatioSums[sectionIndex] += (1 / aspectRatio) 92 | } 93 | } 94 | } 95 | return sectionedArtworks 96 | } 97 | 98 | renderSections() { 99 | // const spacerStyle = { 100 | // height: this.props.itemMargin, 101 | // } 102 | const sectionedArtworks = this.sectionedArtworks() 103 | const sections = [] 104 | for (let i = 0; i < this.state.sectionCount; i++) { 105 | const artworkComponents = [] 106 | const artworks = sectionedArtworks[i] 107 | for (let j = 0; j < artworks.length; j++) { 108 | const artwork = artworks[j] 109 | artworkComponents.push() 110 | if (j < artworks.length - 1) { 111 | // artworkComponents.push(
) 112 | artworkComponents.push(
) 113 | } 114 | } 115 | 116 | // const sectionSpecificStlye = { 117 | // width: this.state.sectionDimension, 118 | // marginRight: (i === this.state.sectionCount - 1 ? 0 : this.props.sectionMargin), 119 | // } 120 | sections.push( 121 | //
122 |
{artworkComponents}
, 123 | ) 124 | } 125 | return sections 126 | } 127 | 128 | render() { 129 | return
{this.renderSections()}
130 | } 131 | } 132 | 133 | // .grid { 134 | // display: flex; 135 | // flex-direction: row; 136 | // } 137 | 138 | // .grid .column { 139 | // flex-direction: column; 140 | // flex: 1 0 0px; 141 | // max-width: 300px; 142 | // margin-right: 20px; 143 | // } 144 | 145 | const styles = StyleSheet.create({ 146 | column: { 147 | flex: "1 0 0px", 148 | flexDirection: "column", 149 | marginRight: 20, 150 | maxWidth: 300, 151 | }, 152 | container: { 153 | display: "flex", 154 | flexDirection: "row", 155 | }, 156 | }) 157 | 158 | export default Relay.createContainer(Grid, { 159 | fragments: { 160 | artworks: () => Relay.QL` 161 | fragment on Artwork @relay(plural: true) { 162 | __id 163 | image { 164 | aspect_ratio 165 | } 166 | ${Artwork.getFragment("artwork")} 167 | } 168 | `, 169 | }, 170 | }) 171 | -------------------------------------------------------------------------------- /app/containers/react-aphrodite/artist/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import ArtistHeader from "../../../components/artist/header" 5 | import Grid from "./grid" 6 | 7 | interface Props { 8 | artist: any 9 | } 10 | interface State { 11 | following: boolean | null, 12 | followersCount: number, 13 | } 14 | 15 | export class Artist extends React.Component { 16 | render() { 17 | return ( 18 |
19 | 20 |
21 | 22 |
23 | ) 24 | } 25 | } 26 | 27 | export default Relay.createContainer(Artist, { 28 | fragments: { 29 | artist: () => Relay.QL` 30 | fragment on Artist { 31 | name 32 | counts { 33 | artworks, 34 | partner_shows 35 | articles 36 | } 37 | artworks(size: 30) { 38 | ${Grid.getFragment("artworks")} 39 | } 40 | ${ArtistHeader.getFragment("artist")} 41 | } 42 | `, 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /app/containers/react-inline-css/artist/artwork.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | interface Props { 5 | artwork: any 6 | } 7 | interface State { 8 | } 9 | 10 | export class Artwork extends React.Component { 11 | render() { 12 | const names = this.props.artwork.artists.map(artist => artist.name) 13 | return ( 14 |
15 | 16 |

{names.join(", ")}

17 |

{this.props.artwork.title}

18 |
19 | ) 20 | } 21 | } 22 | 23 | // .grid .column .artwork { 24 | // flex: 1; 25 | // margin-bottom: 20px; 26 | // } 27 | 28 | // .artwork img { 29 | // width: 100%; 30 | // } 31 | 32 | // .artwork h3,h4 { 33 | // margin: 4px 0; 34 | // color: rgb(102, 102, 102); 35 | // font-size: 15px; 36 | // } 37 | 38 | // .artwork h3 { 39 | // font-style: normal; 40 | // font-weight: bold; 41 | // } 42 | 43 | // .artwork h4 { 44 | // font-style: italic; 45 | // font-weight: normal 46 | // } 47 | 48 | const styles = { 49 | artists: { 50 | fontStyle: "normal", 51 | fontWeight: "bold", 52 | }, 53 | container: { 54 | flex: 1, 55 | marginBottom: 20, 56 | }, 57 | heading: { 58 | color: "rgb(102, 102, 102)", 59 | fontSize: 15, 60 | margin: "4px 0", 61 | }, 62 | image: { 63 | width: "100%", 64 | }, 65 | title: { 66 | fontStyle: "italic", 67 | fontWeight: "normal", 68 | }, 69 | } 70 | 71 | export default Relay.createContainer(Artwork, { 72 | fragments: { 73 | artwork: () => Relay.QL` 74 | fragment on Artwork { 75 | title 76 | artists { 77 | name 78 | } 79 | image { 80 | url 81 | } 82 | } 83 | `, 84 | }, 85 | }) 86 | -------------------------------------------------------------------------------- /app/containers/react-inline-css/artist/browser.tsx: -------------------------------------------------------------------------------- 1 | import IsomorphicRelay from "isomorphic-relay" 2 | import React from "react" 3 | import ReactDOM from "react-dom" 4 | 5 | import { artsyRelayEnvironment } from "../../../relay/config" 6 | import { ArtistQueryConfig } from "../../../relay/root_queries" 7 | 8 | import Artist from "./index" 9 | 10 | const rootElement = document.getElementById("root") 11 | 12 | declare var window: any 13 | 14 | const environment = artsyRelayEnvironment() 15 | IsomorphicRelay.injectPreparedData(environment, window.ARTIST_PROPS) 16 | 17 | IsomorphicRelay.prepareInitialRender({ 18 | Container: Artist, 19 | queryConfig: new ArtistQueryConfig({ artistID: window.ARTIST_ID }), 20 | environment, 21 | }).then(props => { 22 | ReactDOM.render(, rootElement) 23 | }) 24 | -------------------------------------------------------------------------------- /app/containers/react-inline-css/artist/grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import Artwork from "./artwork" 5 | 6 | interface Props { 7 | sectionMargin: number, 8 | itemMargin: number, 9 | artworks: any[] 10 | } 11 | 12 | interface State { 13 | sectionCount: number, 14 | } 15 | 16 | export class Grid extends React.Component { 17 | // state: { 18 | // sectionDimension: number, 19 | // sectionCount: number, 20 | // }; 21 | 22 | static defaultProps = { 23 | // sectionDirection: "column", 24 | itemMargin: 20, 25 | sectionMargin: 20, 26 | } 27 | 28 | constructor(props) { 29 | super(props) 30 | this.state = { 31 | // sectionDimension: 0, 32 | sectionCount: 3, 33 | } 34 | 35 | // this.onLayout = this.onLayout.bind(this) 36 | } 37 | 38 | // layoutState(currentLayout) : Object { 39 | // const width = currentLayout.width 40 | // const isPad = width > 600 41 | // const isPadHorizontal = width > 900 42 | 43 | // const sectionCount = isPad ? (isPadHorizontal ? 4 : 3) : 2 44 | // const sectionMargins = this.props.sectionMargin * (sectionCount - 1) 45 | // const sectionDimension = (currentLayout.width - sectionMargins) / sectionCount 46 | 47 | // return { sectionCount: sectionCount, 48 | // sectionDimension: sectionDimension, 49 | // } 50 | // } 51 | 52 | // onLayout = (event: LayoutEvent) => { 53 | // const layout = event.nativeEvent.layout 54 | // const newLayoutState = this.layoutState(layout) 55 | // if (layout.width > 0) { 56 | // this.setState(newLayoutState) 57 | // } 58 | // } 59 | 60 | sectionedArtworks() { 61 | const sectionedArtworks = [] 62 | const sectionRatioSums = [] 63 | for (let i = 0; i < this.state.sectionCount; i++) { 64 | sectionedArtworks.push([]) 65 | sectionRatioSums.push(0) 66 | } 67 | 68 | const artworks = this.props.artworks 69 | for (const artwork of artworks) { 70 | 71 | if (artwork.image) { 72 | let lowestRatioSum = Number.MAX_VALUE 73 | let sectionIndex: number | null = null 74 | 75 | for (let j = 0; j < sectionRatioSums.length; j++) { 76 | const ratioSum = sectionRatioSums[j] 77 | if (ratioSum < lowestRatioSum) { 78 | sectionIndex = j 79 | lowestRatioSum = ratioSum 80 | } 81 | } 82 | 83 | if (sectionIndex != null) { 84 | const section = sectionedArtworks[sectionIndex] 85 | section.push(artwork) 86 | 87 | // total section aspect ratio 88 | const aspectRatio = artwork.image.aspect_ratio || 1 89 | sectionRatioSums[sectionIndex] += (1 / aspectRatio) 90 | } 91 | } 92 | } 93 | return sectionedArtworks 94 | } 95 | 96 | renderSections() { 97 | // const spacerStyle = { 98 | // height: this.props.itemMargin, 99 | // } 100 | const sectionedArtworks = this.sectionedArtworks() 101 | const sections = [] 102 | for (let i = 0; i < this.state.sectionCount; i++) { 103 | const artworkComponents = [] 104 | const artworks = sectionedArtworks[i] 105 | for (let j = 0; j < artworks.length; j++) { 106 | const artwork = artworks[j] 107 | artworkComponents.push() 108 | if (j < artworks.length - 1) { 109 | // artworkComponents.push(
) 110 | artworkComponents.push(
) 111 | } 112 | } 113 | 114 | // const sectionSpecificStlye = { 115 | // width: this.state.sectionDimension, 116 | // marginRight: (i === this.state.sectionCount - 1 ? 0 : this.props.sectionMargin), 117 | // } 118 | sections.push( 119 | //
120 |
121 | {artworkComponents} 122 |
123 | ) 124 | } 125 | return sections 126 | } 127 | 128 | render() { 129 | return
{this.renderSections()}
130 | } 131 | } 132 | 133 | // .grid { 134 | // display: flex; 135 | // flex-direction: row; 136 | // } 137 | 138 | // .grid .column { 139 | // flex-direction: column; 140 | // flex: 1 0 0px; 141 | // max-width: 300px; 142 | // margin-right: 20px; 143 | // } 144 | 145 | const styles = { 146 | column: { 147 | flex: "1 0 0px", 148 | flexDirection: "column", 149 | marginRight: 20, 150 | maxWidth: 300, 151 | }, 152 | container: { 153 | display: "flex", 154 | flexDirection: "row", 155 | }, 156 | } 157 | 158 | export default Relay.createContainer(Grid, { 159 | fragments: { 160 | artworks: () => Relay.QL` 161 | fragment on Artwork @relay(plural: true) { 162 | __id 163 | image { 164 | aspect_ratio 165 | } 166 | ${Artwork.getFragment("artwork")} 167 | } 168 | `, 169 | }, 170 | }) 171 | -------------------------------------------------------------------------------- /app/containers/react-inline-css/artist/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import ArtistHeader from "../../../components/artist/header" 5 | import Grid from "./grid" 6 | 7 | interface Props { 8 | artist: any 9 | } 10 | interface State { 11 | following: boolean | null, 12 | followersCount: number, 13 | } 14 | 15 | export class Artist extends React.Component { 16 | render() { 17 | return ( 18 |
19 | 20 |
21 | 22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default Relay.createContainer(Artist, { 29 | fragments: { 30 | artist: () => Relay.QL` 31 | fragment on Artist { 32 | name 33 | counts { 34 | artworks, 35 | partner_shows 36 | articles 37 | } 38 | artworks(size: 30) { 39 | ${Grid.getFragment("artworks")} 40 | } 41 | ${ArtistHeader.getFragment("artist")} 42 | } 43 | `, 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /app/containers/react-jss/artist/artwork.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import injectSheet from "react-jss" 5 | 6 | interface Props { 7 | artwork: any 8 | sheet: any 9 | } 10 | interface State { 11 | } 12 | 13 | export class Artwork extends React.Component { 14 | render() { 15 | const classes = this.props.sheet.classes 16 | const names = this.props.artwork.artists.map(artist => artist.name) 17 | return ( 18 |
19 | 20 |

{names.join(", ")}

21 |

{this.props.artwork.title}

22 |
23 | ) 24 | } 25 | } 26 | 27 | // .grid .column .artwork { 28 | // flex: 1; 29 | // margin-bottom: 20px; 30 | // } 31 | 32 | // .artwork img { 33 | // width: 100%; 34 | // } 35 | 36 | // .artwork h3,h4 { 37 | // margin: 4px 0; 38 | // color: rgb(102, 102, 102); 39 | // font-size: 15px; 40 | // } 41 | 42 | // .artwork h3 { 43 | // font-style: normal; 44 | // font-weight: bold; 45 | // } 46 | 47 | // .artwork h4 { 48 | // font-style: italic; 49 | // font-weight: normal 50 | // } 51 | 52 | const styles = { 53 | artists: { 54 | fontStyle: "normal", 55 | fontWeight: "bold", 56 | }, 57 | container: { 58 | flex: 1, 59 | marginBottom: 20, 60 | }, 61 | heading: { 62 | color: "rgb(102, 102, 102)", 63 | fontSize: 15, 64 | margin: "4px 0", 65 | }, 66 | image: { 67 | width: "100%", 68 | }, 69 | title: { 70 | fontStyle: "italic", 71 | fontWeight: "normal", 72 | }, 73 | } 74 | 75 | export default Relay.createContainer(injectSheet(styles)(Artwork), { 76 | fragments: { 77 | artwork: () => Relay.QL` 78 | fragment on Artwork { 79 | title 80 | artists { 81 | name 82 | } 83 | image { 84 | url 85 | } 86 | } 87 | `, 88 | }, 89 | }) 90 | -------------------------------------------------------------------------------- /app/containers/react-jss/artist/browser.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | import IsomorphicRelay from "isomorphic-relay" 5 | 6 | import { artsyRelayEnvironment } from "../../../relay/config" 7 | import { ArtistQueryConfig } from "../../../relay/root_queries" 8 | 9 | import Artist from "./index" 10 | 11 | const rootElement = document.getElementById("root") 12 | declare var window: any 13 | 14 | const environment = artsyRelayEnvironment() 15 | IsomorphicRelay.injectPreparedData(environment, window.ARTIST_PROPS) 16 | 17 | IsomorphicRelay.prepareInitialRender({ 18 | Container: Artist, 19 | queryConfig: new ArtistQueryConfig({ artistID: window.ARTIST_ID }), 20 | environment, 21 | }).then(props => { 22 | ReactDOM.render(, rootElement) 23 | }) 24 | -------------------------------------------------------------------------------- /app/containers/react-jss/artist/grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import injectSheet from "react-jss" 5 | 6 | import Artwork from "./artwork" 7 | 8 | interface Props { 9 | sectionMargin: number, 10 | itemMargin: number, 11 | artworks: any[], 12 | sheet: any 13 | } 14 | interface State { 15 | sectionCount: number, 16 | } 17 | 18 | export class Grid extends React.Component { 19 | // sectionDimension: number, 20 | // sectionCount: number, 21 | // }; 22 | 23 | static defaultProps = { 24 | // sectionDirection: "column", 25 | itemMargin: 20, 26 | sectionMargin: 20, 27 | } 28 | 29 | constructor(props) { 30 | super(props) 31 | this.state = { 32 | // sectionDimension: 0, 33 | sectionCount: 3, 34 | } 35 | 36 | // this.onLayout = this.onLayout.bind(this) 37 | } 38 | 39 | // layoutState(currentLayout) : Object { 40 | // const width = currentLayout.width 41 | // const isPad = width > 600 42 | // const isPadHorizontal = width > 900 43 | 44 | // const sectionCount = isPad ? (isPadHorizontal ? 4 : 3) : 2 45 | // const sectionMargins = this.props.sectionMargin * (sectionCount - 1) 46 | // const sectionDimension = (currentLayout.width - sectionMargins) / sectionCount 47 | 48 | // return { sectionCount: sectionCount, 49 | // sectionDimension: sectionDimension, 50 | // } 51 | // } 52 | 53 | // onLayout = (event: LayoutEvent) => { 54 | // const layout = event.nativeEvent.layout 55 | // const newLayoutState = this.layoutState(layout) 56 | // if (layout.width > 0) { 57 | // this.setState(newLayoutState) 58 | // } 59 | // } 60 | 61 | sectionedArtworks() { 62 | const sectionedArtworks = [] 63 | const sectionRatioSums = [] 64 | for (let i = 0; i < this.state.sectionCount; i++) { 65 | sectionedArtworks.push([]) 66 | sectionRatioSums.push(0) 67 | } 68 | 69 | const artworks = this.props.artworks 70 | for (const artwork of artworks) { 71 | 72 | if (artwork.image) { 73 | let lowestRatioSum = Number.MAX_VALUE 74 | let sectionIndex: number | null = null 75 | 76 | for (let j = 0; j < sectionRatioSums.length; j++) { 77 | const ratioSum = sectionRatioSums[j] 78 | if (ratioSum < lowestRatioSum) { 79 | sectionIndex = j 80 | lowestRatioSum = ratioSum 81 | } 82 | } 83 | 84 | if (sectionIndex != null) { 85 | const section = sectionedArtworks[sectionIndex] 86 | section.push(artwork) 87 | 88 | // total section aspect ratio 89 | const aspectRatio = artwork.image.aspect_ratio || 1 90 | sectionRatioSums[sectionIndex] += (1 / aspectRatio) 91 | } 92 | } 93 | } 94 | return sectionedArtworks 95 | } 96 | 97 | renderSections() { 98 | // const spacerStyle = { 99 | // height: this.props.itemMargin, 100 | // } 101 | const sectionedArtworks = this.sectionedArtworks() 102 | const sections = [] 103 | for (let i = 0; i < this.state.sectionCount; i++) { 104 | const artworkComponents = [] 105 | const artworks = sectionedArtworks[i] 106 | for (let j = 0; j < artworks.length; j++) { 107 | const artwork = artworks[j] 108 | artworkComponents.push() 109 | if (j < artworks.length - 1) { 110 | // artworkComponents.push(
) 111 | artworkComponents.push(
) 112 | } 113 | } 114 | 115 | // const sectionSpecificStlye = { 116 | // width: this.state.sectionDimension, 117 | // marginRight: (i === this.state.sectionCount - 1 ? 0 : this.props.sectionMargin), 118 | // } 119 | sections.push( 120 | //
121 |
122 | {artworkComponents} 123 |
124 | ) 125 | } 126 | return sections 127 | } 128 | 129 | render() { 130 | return
{this.renderSections()}
131 | } 132 | } 133 | 134 | // .grid { 135 | // display: flex; 136 | // flex-direction: row; 137 | // } 138 | 139 | // .grid .column { 140 | // flex-direction: column; 141 | // flex: 1 0 0px; 142 | // max-width: 300px; 143 | // margin-right: 20px; 144 | // } 145 | 146 | const styles = { 147 | column: { 148 | flex: "1 0 0px", 149 | flexDirection: "column", 150 | marginRight: 20, 151 | maxWidth: 300, 152 | }, 153 | container: { 154 | display: "flex", 155 | flexDirection: "row", 156 | }, 157 | } 158 | 159 | export default Relay.createContainer(injectSheet(styles)(Grid), { 160 | fragments: { 161 | artworks: () => Relay.QL` 162 | fragment on Artwork @relay(plural: true) { 163 | __id 164 | image { 165 | aspect_ratio 166 | } 167 | ${Artwork.getFragment("artwork")} 168 | } 169 | `, 170 | }, 171 | }) 172 | -------------------------------------------------------------------------------- /app/containers/react-jss/artist/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as Relay from "react-relay" 3 | 4 | import ArtistHeader from "../../../components/artist/header" 5 | import Grid from "./grid" 6 | 7 | interface Props { 8 | artist: any 9 | } 10 | interface State { 11 | following: boolean | null, 12 | followersCount: number, 13 | } 14 | 15 | export class Artist extends React.Component { 16 | render() { 17 | return ( 18 |
19 | 20 |
21 | 22 |
23 | ) 24 | } 25 | } 26 | 27 | export default Relay.createContainer(Artist, { 28 | fragments: { 29 | artist: () => Relay.QL` 30 | fragment on Artist { 31 | name 32 | counts { 33 | artworks, 34 | partner_shows 35 | articles 36 | } 37 | artworks(size: 30) { 38 | ${Grid.getFragment("artworks")} 39 | } 40 | ${ArtistHeader.getFragment("artist")} 41 | } 42 | `, 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/artwork.tsx: -------------------------------------------------------------------------------- 1 | import { map } from "lodash" 2 | import * as React from "react" 3 | import { Image, StyleSheet, TextStyle, TouchableWithoutFeedback, View, ViewStyle } from "react-native-web" 4 | import * as Relay from "react-relay" 5 | 6 | // import ImageView from '../opaque_image_view' 7 | const ImageView = (props: { style: ViewStyle, aspectRatio: number, imageURL: string }) => { 8 | return 9 | } 10 | 11 | import GQL from "../../../gql" 12 | import SerifText from "./text/serif" 13 | 14 | import * as colors from "../../../../data/colors.json" 15 | 16 | import SwitchBoard from "./switch_board" 17 | 18 | interface Props { 19 | artwork: GQL.ArtworkType, 20 | } 21 | 22 | class Artwork extends React.Component { 23 | handleTap() { 24 | SwitchBoard.presentNavigationViewController(this, this.props.artwork.href) 25 | } 26 | 27 | render() { 28 | const artwork = this.props.artwork 29 | return ( 30 | 31 | 32 | 33 | {this.artists()} 34 | {this.artworkTitle()} 35 | {this.props.artwork.partner && {this.props.artwork.partner.name}} 36 | {this.saleMessage()} 37 | 38 | 39 | ) 40 | } 41 | 42 | artists() { 43 | const artists = this.props.artwork.artists 44 | if (artists && artists.length > 0) { 45 | return ( 46 | 47 | {map(artists, "name").join(", ")} 48 | 49 | ) 50 | } else { 51 | return null 52 | } 53 | } 54 | 55 | artworkTitle() { 56 | const artwork = this.props.artwork 57 | if (artwork.title) { 58 | return ( 59 | 60 | {artwork.title} 61 | {artwork.date ? (", " + artwork.date) : ""} 62 | 63 | ) 64 | } else { 65 | return null 66 | } 67 | } 68 | 69 | saleMessage() { 70 | const artwork = this.props.artwork 71 | if (artwork.is_in_auction) { 72 | return ( 73 | 74 | {/**/} 75 | Bid now 76 | 77 | ) 78 | } else { 79 | return artwork.sale_message && {artwork.sale_message} 80 | } 81 | } 82 | } 83 | 84 | interface Styles { 85 | artist: TextStyle, 86 | image: ViewStyle, 87 | text: TextStyle, 88 | title: TextStyle, 89 | } 90 | 91 | const styles = StyleSheet.create({ 92 | artist: { 93 | fontWeight: "bold", 94 | }, 95 | image: { 96 | marginBottom: 10, 97 | }, 98 | text: { 99 | color: colors["gray-semibold"], 100 | fontSize: 12, 101 | }, 102 | title: { 103 | fontStyle: "italic", 104 | } 105 | }) 106 | 107 | export default Relay.createContainer(Artwork, { 108 | fragments: { 109 | artwork: () => Relay.QL` 110 | fragment on Artwork { 111 | title 112 | date 113 | sale_message 114 | is_in_auction 115 | image { 116 | url(version: "large") 117 | aspect_ratio 118 | } 119 | artists { 120 | name 121 | } 122 | partner { 123 | name 124 | } 125 | href 126 | } 127 | ` 128 | } 129 | }) 130 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/browser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ReactDOM from "react-dom" 3 | 4 | import IsomorphicRelay from "isomorphic-relay" 5 | 6 | import { artsyRelayEnvironment } from "../../../relay/config" 7 | import { ArtistQueryConfig } from "../../../relay/root_queries" 8 | 9 | import Artist from "./index" 10 | 11 | const rootElement = document.getElementById("root") 12 | 13 | declare var window: any 14 | 15 | const environment = artsyRelayEnvironment() 16 | IsomorphicRelay.injectPreparedData(environment, window.ARTIST_PROPS) 17 | 18 | IsomorphicRelay.prepareInitialRender({ 19 | Container: Artist, 20 | queryConfig: new ArtistQueryConfig({ artistID: window.ARTIST_ID }), 21 | environment, 22 | }).then(props => { 23 | ReactDOM.render(, rootElement) 24 | }) 25 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/grid.tsx: -------------------------------------------------------------------------------- 1 | // 1. Get first layout pass of grid view so we have a total width and calculate the column width (componentDidMount?). 2 | // 2. Possibly do artwork column layout now, as we can do so based just on the aspect ratio, assuming the text height 3 | // won't be too different between artworks. 4 | // 3. Get artwork heights by either: 5 | // - calculating the item size upfront with aspect ratio and a static height for the text labels. 6 | // - leting the artwork component do a layout pass and calculate its own height based on the column width. 7 | // 4. Update height of grid to encompass all items. 8 | 9 | import * as React from "react" 10 | import { Dimensions, ScrollView, StyleSheet, View, ViewStyle } from "react-native-web" 11 | import * as Relay from "react-relay" 12 | 13 | import Artwork from "./artwork" 14 | // import Spinner from '../spinner' 15 | const Spinner = ({ spinnerColor = null, style }) =>
16 | 17 | import GQL from "../../../gql" 18 | import metaphysics from "./metaphysics" 19 | 20 | import { get, isEqual } from "lodash" 21 | 22 | // const isPad = Dimensions.get('window').width > 700 23 | const isPad = true 24 | 25 | const PageSize = 10 26 | const PageEndThreshold = 1000 27 | 28 | /** 29 | * TODOs: 30 | * - currently all the code assumes column layout 31 | * - do no invert aspect ratios in row layout 32 | * - deal with edge-cases when calculating in which section an artwork should go 33 | * - see ARMasonryCollectionViewLayout for details on how to deal with last works sticking out 34 | * - the calculation currently only takes into account the size of the image, not if e.g. the sale message is present 35 | */ 36 | 37 | interface Props { 38 | /** The direction for the grid, currently only 'column' is supported . */ 39 | sectionDirection: string, 40 | 41 | /** The arity of the number of sections (e.g. columns) to show */ 42 | sectionCount: number, 43 | 44 | /** The inset margin for the whole grid */ 45 | sectionMargin: number, 46 | 47 | /** The per-item margin */ 48 | itemMargin: number, 49 | 50 | /** All the artworks for the grid */ 51 | artworks: GQL.ArtworkType[], 52 | 53 | /** A non-optional object for the request state. 54 | * When this changes, it will reset the component. 55 | * We recommend sending in your query params. 56 | * This gets passed back to your request query below. 57 | */ 58 | queryState: any, 59 | 60 | /** A non-optional callback to generate the GraphQL query. */ 61 | queryForPage: (component: InfiniteScrollArtworksGrid, page: number, queryState: any) => string, 62 | 63 | /** A callback that is called once all artworks have been queried. */ 64 | onComplete?: () => void, 65 | 66 | /** When you get the results from the GraphQL, this is the keypath from 67 | * which the artworks can be found, applied via `_.get()` 68 | */ 69 | queryArtworksKeypath: string, 70 | } 71 | 72 | interface State { 73 | sectionDimension: number, 74 | artworks: GQL.ArtworkType[], 75 | page: number, 76 | completed: boolean, 77 | fetchingNextPage: boolean, 78 | sentEndForContentLength: null | number 79 | } 80 | 81 | class InfiniteScrollArtworksGrid extends React.Component { 82 | static defaultProps = { 83 | itemMargin: 20, 84 | sectionCount: isPad ? 3 : 2, 85 | sectionDirection: "column", 86 | sectionMargin: 20, 87 | } 88 | 89 | constructor(props) { 90 | super(props) 91 | this.state = { 92 | artworks: this.props.artworks, 93 | completed: false, 94 | fetchingNextPage: false, 95 | page: this.props.artworks.length ? 1 : 0, 96 | sectionDimension: 0, 97 | sentEndForContentLength: null, 98 | } 99 | } 100 | 101 | // Initial setup 102 | componentWillMount() { 103 | if (this.state.artworks.length === 0) { 104 | this.fetchNextPage() 105 | } 106 | } 107 | 108 | // Reset detection 109 | componentDidUpdate() { 110 | if (this.state.artworks.length === 0) { 111 | this.fetchNextPage() 112 | } 113 | } 114 | 115 | /** Download new Artworks, and update the internal state accordingly */ 116 | fetchNextPage() { 117 | if (this.state.fetchingNextPage || this.state.completed) { 118 | return 119 | } 120 | 121 | const nextPage = this.state.page + 1 122 | const queryState = this.props.queryState 123 | const query = this.props.queryForPage(this, nextPage, queryState) 124 | 125 | metaphysics(query) 126 | .then((results) => { 127 | // this.debugLog(query, results, null) 128 | 129 | const artworks: GQL.ArtworkType[] = get(results, this.props.queryArtworksKeypath) 130 | if (artworks === undefined) { 131 | console.error("Your queryArtworksKeypath could be wrong in the infinite_scroll_grid") 132 | } 133 | const completed = artworks.length < PageSize 134 | if (completed && this.props.onComplete) { 135 | this.props.onComplete() 136 | } 137 | this.setState({ 138 | artworks: this.state.artworks.concat(artworks), 139 | completed, 140 | fetchingNextPage: false, 141 | page: nextPage, 142 | } as State) // TODO Required until this is merged: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/13155 143 | }) 144 | .catch((error) => { 145 | this.setState({ fetchingNextPage: false } as State) 146 | // this.debugLog(query, null, error) 147 | }) 148 | 149 | this.setState({ fetchingNextPage: true } as State) 150 | } 151 | 152 | /** A simplified version of the Relay debugging logs for infinite scrolls */ 153 | // debugLog(query: string, response?: any, error?: any) { 154 | // if (__DEV__ && global.originalXMLHttpRequest !== undefined) { 155 | // var groupName = '%c[' + this.state.page + '] ' + 'Infinite scroll request' 156 | // console.groupCollapsed(groupName, 'color:' + (response ? 'black' : 'red') + ';') 157 | // console.log('Query:\n', query) 158 | // if (response) { 159 | // console.log('Response:\n', response) 160 | // } 161 | // console.groupEnd() 162 | // if (error) { 163 | // console.error('Error:\n', error) 164 | // } 165 | // } 166 | // } 167 | 168 | shouldComponentUpdate(nextProps, nextState) { 169 | if (this.props === undefined) { 170 | return true 171 | } 172 | 173 | if (!isEqual(this.props.queryState, nextProps.queryState)) { 174 | // Empty the artworks, and reset the state as we have new a query object 175 | this.setState({ 176 | artworks: [], 177 | completed: false, 178 | fetchingNextPage: false, 179 | page: 0, 180 | } as State) 181 | return true 182 | } 183 | 184 | return (!isEqual(this.props, nextProps) || !isEqual(this.state, nextState)) 185 | } 186 | 187 | // onLayout = (event: React.LayoutChangeEvent) => { 188 | // const layout = event.nativeEvent.layout 189 | // if (layout.width > 0) { 190 | // // This is the sum of all margins in between sections, so do not count to the right of last column. 191 | // const sectionMargins = this.props.sectionMargin * (this.props.sectionCount - 1) 192 | // this.setState({ sectionDimension: (layout.width - sectionMargins) / this.props.sectionCount } as State) 193 | // } 194 | // } 195 | 196 | sectionedArtworks() { 197 | const sectionedArtworks: GQL.ArtworkType[][] = [] 198 | const sectionRatioSums: number[] = [] 199 | for (let i = 0; i < this.props.sectionCount; i++) { 200 | sectionedArtworks.push([]) 201 | sectionRatioSums.push(0) 202 | } 203 | 204 | const artworks = this.state.artworks 205 | for (let i = 0; i < artworks.length; i++) { 206 | const artwork = this.state.artworks[i] 207 | 208 | // There are artworks without images and other ‘issues’. Like Force we’re just going to reject those for now. 209 | // See: https://github.com/artsy/eigen/issues/1667 210 | // 211 | if (artwork.image) { 212 | // Find section with lowest *inverted* aspect ratio sum, which is the shortest column. 213 | let lowestRatioSum = Number.MAX_VALUE // Start higher, so we always find a 214 | let sectionIndex: number = null 215 | for (let j = 0; j < sectionRatioSums.length; j++) { 216 | const ratioSum = sectionRatioSums[j] 217 | if (ratioSum < lowestRatioSum) { 218 | sectionIndex = j 219 | lowestRatioSum = ratioSum 220 | } 221 | } 222 | 223 | if (sectionIndex != null) { 224 | const section = sectionedArtworks[sectionIndex] 225 | section.push(artwork) 226 | 227 | // Keep track of total section aspect ratio 228 | const aspectRatio = artwork.image.aspect_ratio || 1 // Ensure we never divide by null/0 229 | // Invert the aspect ratio so that a lower value means a shorter section. 230 | sectionRatioSums[sectionIndex] += (1 / aspectRatio) 231 | } 232 | } 233 | } 234 | 235 | return sectionedArtworks 236 | } 237 | 238 | renderSections() { 239 | const spacerStyle = { 240 | height: this.props.itemMargin, 241 | } 242 | 243 | const sectionedArtworks = this.sectionedArtworks() 244 | const sections = [] 245 | for (let i = 0; i < this.props.sectionCount; i++) { 246 | const artworkComponents = [] 247 | const artworks = sectionedArtworks[i] 248 | for (let j = 0; j < artworks.length; j++) { 249 | const artwork = artworks[j] 250 | artworkComponents.push() 251 | // Setting a marginBottom on the artwork component didn’t work, so using a spacer view instead. 252 | if (j < artworks.length - 1) { 253 | artworkComponents.push( 254 | 255 | ) 256 | } 257 | } 258 | 259 | const sectionSpecificStyle = { 260 | marginRight: (i === this.props.sectionCount - 1 ? 0 : this.props.sectionMargin), 261 | width: this.state.sectionDimension, 262 | } 263 | 264 | sections.push( 265 | 266 | {artworkComponents} 267 | 268 | ) 269 | } 270 | return sections 271 | } 272 | 273 | // Lifted pretty much straight from RN’s ListView.js 274 | onScroll = (event: React.NativeSyntheticEvent) => { 275 | const scrollProperties = event.nativeEvent 276 | const contentLength = scrollProperties.contentSize.height 277 | if (contentLength !== this.state.sentEndForContentLength) { 278 | const offset = scrollProperties.contentOffset.y 279 | const visibleLength = scrollProperties.layoutMeasurement.height 280 | const distanceFromEnd = contentLength - visibleLength - offset 281 | if (distanceFromEnd < PageEndThreshold) { 282 | this.state.sentEndForContentLength = contentLength 283 | this.fetchNextPage() 284 | } 285 | } 286 | } 287 | 288 | render() { 289 | // const artworks = this.state.sectionDimension ? this.renderSections() : null 290 | const artworks = this.renderSections() 291 | return ( 292 | 297 | 298 | {artworks} 299 | 300 | {this.state.fetchingNextPage ? : null} 301 | 302 | ) 303 | } 304 | } 305 | 306 | interface Styles { 307 | container: ViewStyle, 308 | section: ViewStyle, 309 | spinner: ViewStyle, 310 | } 311 | 312 | const styles = StyleSheet.create({ 313 | container: { 314 | display: "flex", 315 | flexDirection: "row", 316 | } as ViewStyle, // FIXME: Needed because `display` is not a prop that non-web RN has. 317 | section: { 318 | // flex: "1 0 0px", 319 | flex: 1, 320 | flexDirection: "column", 321 | marginRight: 20, 322 | maxWidth: 300, 323 | }, 324 | spinner: { 325 | marginTop: 20, 326 | }, 327 | }) 328 | 329 | const InfiniteScrollArtworksGridContainer = Relay.createContainer(InfiniteScrollArtworksGrid, { 330 | fragments: { 331 | artworks: () => Relay.QL` 332 | fragment on Artwork @relay(plural: true) { 333 | __id 334 | image { 335 | aspect_ratio 336 | } 337 | ${Artwork.getFragment("artwork")} 338 | } 339 | `, 340 | } 341 | }) 342 | 343 | // TODO: While we do pagination manually, we can’t actually use a Relay container around Artwork. 344 | const container: any = InfiniteScrollArtworksGridContainer 345 | container.artworksQuery = (artistID, filter, page) => { 346 | return ` 347 | query { 348 | artist(id: "${artistID}") { 349 | artworks(sort: partner_updated_at_desc, filter: ${filter} size: ${PageSize}, page: ${page}) { 350 | __id 351 | title 352 | date 353 | sale_message 354 | is_in_auction 355 | image { 356 | url(version: "large") 357 | aspect_ratio 358 | } 359 | artist { 360 | name 361 | } 362 | partner { 363 | name 364 | } 365 | href 366 | } 367 | } 368 | } 369 | ` 370 | } 371 | 372 | export default InfiniteScrollArtworksGridContainer 373 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Dimensions, FlexAlignType, StyleSheet, Text, TextStyle, View, ViewStyle } from "react-native-web" 3 | import * as Relay from "react-relay" 4 | 5 | // import Events from '../../native_modules/events' 6 | 7 | import * as colors from "../../../../data/colors.json" 8 | 9 | import GQL from "../../../gql" 10 | 11 | import InvertedButton from "./inverted_button" 12 | import Headline from "./text/headline" 13 | import SerifText from "./text/serif" 14 | 15 | // const isPad = Dimensions.get('window').width > 700 16 | const isPad = true 17 | 18 | interface Props { 19 | artist: GQL.ArtistType, 20 | } 21 | 22 | interface State { 23 | following: boolean | null, 24 | followersCount: number, 25 | } 26 | 27 | class Header extends React.Component { 28 | constructor(props) { 29 | super(props) 30 | this.state = { following: true, followersCount: props.artist.counts.follows } 31 | } 32 | 33 | // componentDidMount() { 34 | // ARTemporaryAPIModule.followStatusForArtist(this.props.artist._id, (error, following) => { 35 | // this.setState({ following: following }) 36 | // }) 37 | // } 38 | 39 | handleFollowChange = () => { 40 | const { following, followersCount } = this.state 41 | const newFollowersCount: number = following ? (followersCount - 1) : (followersCount + 1) 42 | // ARTemporaryAPIModule.setFollowArtistStatus(!this.state.following, this.props.artist._id, (error, following) => { 43 | // if (error) { 44 | // console.error(error) 45 | // } else { 46 | // Events.postEvent(this, { 47 | // name: following ? 'Follow artist' : 'Unfollow artist', 48 | // artist_id: this.props.artist._id, 49 | // artist_slug: this.props.artist.id, 50 | // // TODO At some point, this component might be on other screens. 51 | // source_screen: 'artist page', 52 | // }) 53 | // } 54 | // this.setState({ following: following, followersCount: newFollowersCount }) 55 | // }) 56 | this.setState({ following: !following, followersCount: newFollowersCount }) 57 | } 58 | 59 | render() { 60 | const artist = this.props.artist 61 | return ( 62 | 63 | 64 | {artist.name} 65 | 66 | {this.renderByline()} 67 | {this.renderFollowersCount()} 68 | {this.renderFollowButton()} 69 | 70 | ) 71 | } 72 | 73 | renderFollowButton() { 74 | if (this.state.following !== null) { 75 | return ( 76 | 77 | 80 | 81 | ) 82 | } 83 | } 84 | 85 | renderFollowersCount() { 86 | const count = this.state.followersCount 87 | const followerString = count + (count === 1 ? " Follower" : " Followers") 88 | return ( 89 | 90 | {followerString} 91 | 92 | ) 93 | } 94 | 95 | renderByline() { 96 | const artist = this.props.artist 97 | const bylineRequired = (artist.nationality || artist.birthday) 98 | if (bylineRequired) { 99 | return ( 100 | 101 | 102 | {this.descriptiveString()} 103 | 104 | 105 | ) 106 | } else { 107 | return null 108 | } 109 | } 110 | 111 | descriptiveString() { 112 | const artist = this.props.artist 113 | const descriptiveString = (artist.nationality || "") + this.birthdayString() 114 | return descriptiveString 115 | } 116 | 117 | birthdayString() { 118 | const birthday = this.props.artist.birthday 119 | if (!birthday) { return "" } 120 | 121 | const leadingSubstring = this.props.artist.nationality ? ", b." : "" 122 | 123 | if (birthday.includes("born")) { 124 | return birthday.replace("born", leadingSubstring) 125 | } else if (birthday.includes("Est.") || birthday.includes("Founded")) { 126 | return " " + birthday 127 | } 128 | 129 | return leadingSubstring + " " + birthday 130 | } 131 | } 132 | 133 | interface Styles { 134 | base: TextStyle, 135 | headline: TextStyle, 136 | followCount: TextStyle, 137 | followButton: ViewStyle, 138 | } 139 | 140 | const styles = StyleSheet.create({ 141 | base: { 142 | textAlign: "center", 143 | }, 144 | followButton: { 145 | alignSelf: isPad ? "center" : null, 146 | height: 40, 147 | marginLeft: 0, 148 | marginRight: 0, 149 | width: isPad ? 330 : null, 150 | }, 151 | followCount: { 152 | color: colors["gray-semibold"], 153 | marginBottom: 30 154 | }, 155 | headline: { 156 | fontSize: 14, 157 | }, 158 | }) 159 | 160 | export default Relay.createContainer(Header as any, { 161 | fragments: { 162 | artist: () => Relay.QL` 163 | fragment on Artist { 164 | _id 165 | id 166 | name 167 | nationality 168 | birthday 169 | counts { 170 | follows 171 | } 172 | } 173 | `, 174 | } 175 | }) 176 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { StyleSheet, TextStyle, View, ViewStyle } from "react-native-web" 3 | import * as Relay from "react-relay" 4 | 5 | import ArtworksGrid from "./grid" 6 | import ArtistHeader from "./header" 7 | import SerifText from "./text/serif" 8 | 9 | import GQL from "../../../gql" 10 | 11 | import * as colors from "../../../../data/colors.json" 12 | 13 | export const Artist = ({ artist }: { artist: GQL.ArtistType }) => ( 14 | 15 | 16 |
17 | 18 | 19 | Works for Sale 20 | ({artist.counts.for_sale_artworks}) 21 | 22 | console.log("Done paging!")} 28 | queryArtworksKeypath="artist.artworks" /> 29 | 30 |
31 | ) 32 | 33 | const resolveQuery = (artistID: string) => { 34 | return (component: any, page: number, state: any): string => { 35 | // The page + 1 is to take into account the fact that we _start_ with results already 36 | const grid: any = ArtworksGrid 37 | return grid.artworksQuery(artistID, state.availability, component.state.page + 1) 38 | } 39 | } 40 | 41 | interface Styles { 42 | appContainer: ViewStyle, 43 | heading: TextStyle, 44 | text: TextStyle, 45 | count: TextStyle, 46 | } 47 | 48 | const styles = StyleSheet.create({ 49 | /** 50 | * Ensure that the application covers the whole screen. 51 | */ 52 | appContainer: { 53 | bottom: 0, 54 | left: 40, 55 | position: "absolute", 56 | right: 40, 57 | top: 0, 58 | }, 59 | count: { 60 | color: colors["gray-semibold"], 61 | }, 62 | heading: { 63 | marginBottom: 20, 64 | }, 65 | text: { 66 | fontSize: 20, 67 | }, 68 | }) 69 | 70 | export default Relay.createContainer(Artist, { 71 | fragments: { 72 | artist: () => Relay.QL` 73 | fragment on Artist { 74 | _id 75 | id 76 | counts { 77 | for_sale_artworks 78 | } 79 | ${ArtistHeader.getFragment("artist")} 80 | artworks(sort: partner_updated_at_desc, filter: IS_FOR_SALE, size: 10) { 81 | ${ArtworksGrid.getFragment("artworks")} 82 | } 83 | } 84 | `, 85 | }, 86 | }) 87 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/inverted_button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Animated, StyleSheet, Text, TouchableHighlight, View } from "react-native-web" 3 | 4 | import Headline from "./text/headline" 5 | 6 | import * as colors from "../../../../data/colors.json" 7 | 8 | // import Spinner from '../spinner' 9 | const Spinner = ({ spinnerColor, style }) =>
SPINNER
10 | 11 | const AnimationDuration = 250 12 | const AnimatedTouchable = Animated.createAnimatedComponent(TouchableHighlight) 13 | const AnimatedHeadline = Animated.createAnimatedComponent(Headline) 14 | 15 | interface Props { 16 | text: string, 17 | selected: boolean, 18 | inProgress?: boolean, 19 | onPress: () => void, 20 | onSelectionAnimationFinished?: () => void, 21 | } 22 | 23 | interface State { 24 | textOpacity: Animated.Value, 25 | backgroundColor: Animated.Value 26 | } 27 | 28 | export default class InvertedButton extends React.Component { 29 | constructor(props: any) { 30 | super(props) 31 | this.state = { 32 | backgroundColor: new Animated.Value(props.selected ? 1 : 0), 33 | textOpacity: new Animated.Value(1), 34 | } 35 | } 36 | 37 | componentWillUpdate(nextProps: any, nextState: any) { 38 | if (this.props.selected !== nextProps.selected) { 39 | nextState.textOpacity.setValue(0) 40 | } 41 | } 42 | 43 | componentDidUpdate(prevProps: any, prevState: any) { 44 | if (this.props.selected !== prevProps.selected) { 45 | const duration = AnimationDuration 46 | Animated.parallel([ 47 | Animated.timing(this.state.textOpacity, { toValue: 1, duration }), 48 | Animated.timing(this.state.backgroundColor, { toValue: this.props.selected ? 1 : 0, duration }), 49 | ]).start(this.props.onSelectionAnimationFinished) 50 | } 51 | } 52 | 53 | render() { 54 | const backgroundColor = this.state.backgroundColor.interpolate({ 55 | inputRange: [0, 1], 56 | outputRange: (["black", colors["purple-regular"]]) 57 | }) 58 | const styling = { 59 | style: [styles.button, { backgroundColor }], 60 | underlayColor: (this.props.selected ? "black" : colors["purple-regular"]), 61 | } 62 | let content = null 63 | if (this.props.inProgress) { 64 | content = 65 | } else { 66 | content = ( 67 | 68 | {this.props.text} 69 | 70 | ) 71 | } 72 | return ( 73 | 74 | {content} 75 | 76 | ) 77 | } 78 | } 79 | 80 | const styles = StyleSheet.create({ 81 | button: { 82 | alignItems: "center", 83 | flex: 1, 84 | justifyContent: "center", 85 | }, 86 | text: { 87 | color: "white" 88 | } 89 | }) 90 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/metaphysics.ts: -------------------------------------------------------------------------------- 1 | // import { NativeModules } from 'react-native' 2 | // const { Emission } = NativeModules 3 | 4 | import { metaphysicsURL } from '../../../relay/config' 5 | import 'isomorphic-fetch' 6 | 7 | class NetworkError extends Error { 8 | response: any 9 | } 10 | 11 | export default function metaphysics(query: string): Promise { 12 | return fetch(metaphysicsURL, { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | // 'X-USER-ID': Emission.userID, 17 | // 'X-ACCESS-TOKEN': Emission.authenticationToken, 18 | }, 19 | body: JSON.stringify({ query }) 20 | }) 21 | .then(response => { 22 | if (response.status >= 200 && response.status < 300) { 23 | return response 24 | } else { 25 | const error = new NetworkError(response.statusText) 26 | error.response = response 27 | throw error 28 | } 29 | }) 30 | .then(response => response.json() as any) 31 | .then(({ data }) => data) 32 | } 33 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/switch_board.ts: -------------------------------------------------------------------------------- 1 | const SwitchBoard = { 2 | presentNavigationViewController: (component, href) => { 3 | window.location.assign(`https://staging.artsy.net${href}`) 4 | } 5 | } 6 | 7 | export default SwitchBoard 8 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/text/headline.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { StyleSheet, Text, TextStyle } from "react-native-web" 3 | 4 | export default (props: { style: TextStyle, children?: string }) => ( 5 | 6 | {props.children.toUpperCase()} 7 | 8 | ) 9 | 10 | interface Styles { 11 | default: TextStyle, 12 | required: TextStyle, 13 | } 14 | 15 | const styles = StyleSheet.create({ 16 | default: { 17 | fontSize: 12, 18 | }, 19 | required: { 20 | fontFamily: [ 21 | "ITC Avant Garde Gothic W04", 22 | "AvantGardeGothicITCW01D 731075", 23 | "AvantGardeGothicITCW01Dm", 24 | "Helvetica", 25 | "sans-serif" 26 | ].join(", "), 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /app/containers/react-native-web/artist/text/serif.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { StyleSheet, Text, TextProperties } from "react-native-web" 3 | 4 | export default (props: TextProperties) => { 5 | // FIXME Only taking `ref` out of `rest` because the TS compiler complains about passing that on. 6 | const { children, style, ref, ...rest } = props 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | 14 | const styles = StyleSheet.create({ 15 | default: { 16 | fontSize: 17 17 | }, 18 | required: { 19 | fontFamily: "Adobe Garamond W08, adobe-garamond-pro, AGaramondPro-Regular, Times New Roman, Times, serif", 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /app/gql.d.ts: -------------------------------------------------------------------------------- 1 | // graphql typescript definitions 2 | 3 | declare namespace GQL { 4 | interface IGraphQLResponseRoot { 5 | data?: RootQueryTypeType; 6 | errors?: Array; 7 | } 8 | 9 | interface IGraphQLResponseError { 10 | message: string; // Required for all errors 11 | locations?: Array; 12 | [propName: string]: any; // 7.2.2 says 'GraphQL servers may provide additional entries to error' 13 | } 14 | 15 | interface IGraphQLResponseErrorLocation { 16 | line: number; 17 | column: number; 18 | } 19 | 20 | /** 21 | description: null 22 | */ 23 | interface RootQueryTypeType { 24 | __typename: string; 25 | /** Fetches an object given its Globally Unique ID */ 26 | node: Node | null; 27 | status: StatusType | null; 28 | /** An Article */ 29 | article: ArticleType | null; 30 | /** A list of Articles */ 31 | articles: Array | null; 32 | /** An Artwork */ 33 | artwork: ArtworkType | null; 34 | /** A list of Artworks */ 35 | artworks: Array | null; 36 | /** An Artist */ 37 | artist: ArtistType | null; 38 | /** A list of Artists */ 39 | artists: Array | null; 40 | /** An External Partner not on the platform */ 41 | external_partner: ExternalPartnerType | null; 42 | /** A Fair */ 43 | fair: FairType | null; 44 | /** A list of Fairs */ 45 | fairs: Array | null; 46 | gene: GeneType | null; 47 | /** Home screen content */ 48 | home_page: HomePageType | null; 49 | /** A Profile */ 50 | profile: ProfileType | null; 51 | /** A collection of OrderedSets */ 52 | ordered_sets: Array | null; 53 | /** A Partner */ 54 | partner: PartnerType | null; 55 | /** A list of Partners */ 56 | partners: Array | null; 57 | /** Partners Elastic Search results */ 58 | filter_partners: FilterPartnersType | null; 59 | /** Artworks Elastic Search results */ 60 | filter_artworks: FilterArtworksType | null; 61 | /** A PartnerCategory */ 62 | partner_category: PartnerCategoryType | null; 63 | /** A list of PartnerCategories */ 64 | partner_categories: Array | null; 65 | /** A Partner Show */ 66 | partner_show: PartnerShowType | null; 67 | /** A list of PartnerShows */ 68 | partner_shows: Array | null; 69 | /** A Sale */ 70 | sale: SaleType | null; 71 | /** A list of Sales */ 72 | sales: Array | null; 73 | /** A Sale Artwork */ 74 | sale_artwork: SaleArtworkType | null; 75 | /** A Search */ 76 | search: SearchType | null; 77 | /** A Show */ 78 | show: ShowType | null; 79 | /** Trending artists */ 80 | trending_artists: TrendingArtistsType | null; 81 | me: MeType | null; 82 | /** Creates, and authorizes, a JWT custom for Causality */ 83 | causality_jwt: string | null; 84 | } 85 | 86 | /** 87 | description: An object with a Globally Unique ID 88 | */ 89 | type Node = ArticleType | ArtworkType | PartnerType | PartnerShowType | ArtistType | ShowType | ArtworkContextPartnerShowType | HighlightedShowType | HighlightedArticleType | GeneType | HomePageArtworkModuleType | HomePageModuleContextGeneType | HomePageArtistModuleType | ArtistItemType | GeneItemType | ArtistSearchEntityType | ArtworkSearchEntityType | PartnerShowSearchEntityType; 90 | 91 | /** 92 | description: An object with a Globally Unique ID 93 | */ 94 | interface NodeType { 95 | __typename: string; 96 | /** The ID of the object. */ 97 | __id: string; 98 | } 99 | 100 | /** 101 | description: null 102 | */ 103 | interface StatusType { 104 | __typename: string; 105 | gravity: StatusGravityType | null; 106 | /** Metaphysics ping */ 107 | ping: boolean | null; 108 | } 109 | 110 | /** 111 | description: Gravity ping 112 | */ 113 | interface StatusGravityType { 114 | __typename: string; 115 | ping: boolean | null; 116 | } 117 | 118 | /** 119 | description: null 120 | */ 121 | interface ArticleType { 122 | __typename: string; 123 | /** A globally unique ID. */ 124 | __id: string; 125 | /** A type-specific ID. */ 126 | id: string; 127 | cached: number | null; 128 | title: string | null; 129 | published_at: string | null; 130 | updated_at: string | null; 131 | thumbnail_title: string | null; 132 | thumbnail_teaser: string | null; 133 | author: AuthorType | null; 134 | thumbnail_image: ImageType | null; 135 | slug: string | null; 136 | href: string | null; 137 | } 138 | 139 | /** 140 | description: null 141 | */ 142 | interface AuthorType { 143 | __typename: string; 144 | /** A globally unique ID. */ 145 | __id: string; 146 | /** A type-specific ID. */ 147 | id: string; 148 | name: string | null; 149 | profile_handle: string | null; 150 | href: string | null; 151 | } 152 | 153 | /** 154 | description: null 155 | */ 156 | interface ImageType { 157 | __typename: string; 158 | /** A type-specific ID. */ 159 | id: string | null; 160 | href: string | null; 161 | title: string | null; 162 | width: number | null; 163 | height: number | null; 164 | orientation: string | null; 165 | aspect_ratio: number | null; 166 | versions: Array | null; 167 | caption: string | null; 168 | is_default: boolean | null; 169 | position: number | null; 170 | url: string | null; 171 | cropped: CroppedImageUrlType | null; 172 | resized: ResizedImageUrlType | null; 173 | deep_zoom: DeepZoomType | null; 174 | is_zoomable: boolean | null; 175 | /** Value to use when `padding-bottom` for fluid image placeholders */ 176 | placeholder: string | null; 177 | } 178 | 179 | /** 180 | description: null 181 | */ 182 | interface CroppedImageUrlType { 183 | __typename: string; 184 | width: number | null; 185 | height: number | null; 186 | url: string | null; 187 | } 188 | 189 | /** 190 | description: null 191 | */ 192 | interface ResizedImageUrlType { 193 | __typename: string; 194 | factor: number | null; 195 | width: number | null; 196 | height: number | null; 197 | url: string | null; 198 | } 199 | 200 | /** 201 | description: null 202 | */ 203 | interface DeepZoomType { 204 | __typename: string; 205 | Image: DeepZoomImageType | null; 206 | } 207 | 208 | /** 209 | description: null 210 | */ 211 | interface DeepZoomImageType { 212 | __typename: string; 213 | xmlns: string | null; 214 | Url: string | null; 215 | Format: string | null; 216 | TileSize: number | null; 217 | Overlap: number | null; 218 | Size: DeepZoomImageSizeType | null; 219 | } 220 | 221 | /** 222 | description: null 223 | */ 224 | interface DeepZoomImageSizeType { 225 | __typename: string; 226 | Width: number | null; 227 | Height: number | null; 228 | } 229 | 230 | /** 231 | description: null 232 | */ 233 | type ArticleSortsEnum = "PUBLISHED_AT_ASC" | "PUBLISHED_AT_DESC"; 234 | 235 | /** 236 | description: null 237 | */ 238 | interface ArtworkType { 239 | __typename: string; 240 | /** A globally unique ID. */ 241 | __id: string; 242 | /** A type-specific ID. */ 243 | id: string; 244 | /** A type-specific Gravity Mongo Document ID. */ 245 | _id: string; 246 | cached: number | null; 247 | to_s: string | null; 248 | href: string | null; 249 | title: string | null; 250 | category: string | null; 251 | medium: string | null; 252 | date: string | null; 253 | image_rights: string | null; 254 | website: string | null; 255 | collecting_institution: string | null; 256 | partner: PartnerType | null; 257 | /** Returns an HTML string representing the embedded content (video) */ 258 | embed: string | null; 259 | can_share_image: boolean | null; 260 | is_embeddable_video: boolean | null; 261 | is_shareable: boolean | null; 262 | is_hangable: boolean | null; 263 | /** True for inquireable artworks that have an exact price. */ 264 | is_purchasable: boolean | null; 265 | /** Do we want to encourage inquiries on this work? */ 266 | is_inquireable: boolean | null; 267 | /** Are we able to display a contact form on artwork pages? */ 268 | is_contactable: boolean | null; 269 | /** Is this artwork part of an auction? */ 270 | is_in_auction: boolean | null; 271 | /** Is this artwork part of a current show */ 272 | is_in_show: boolean | null; 273 | is_for_sale: boolean | null; 274 | /** Is this artwork part of an auction that is currently running? */ 275 | is_biddable: boolean | null; 276 | is_unique: boolean | null; 277 | is_sold: boolean | null; 278 | is_ecommerce: boolean | null; 279 | /** Whether work can be purchased through e-commerce */ 280 | is_acquireable: boolean | null; 281 | /** When in an auction, can the work be bought before any bids are placed */ 282 | is_buy_nowable: boolean | null; 283 | is_comparable_with_auction_results: boolean | null; 284 | is_downloadable: boolean | null; 285 | is_price_hidden: boolean | null; 286 | is_price_range: boolean | null; 287 | availability: string | null; 288 | sale_message: string | null; 289 | artist: ArtistType | null; 290 | price: string | null; 291 | contact_label: string | null; 292 | cultural_maker: string | null; 293 | artists: Array | null; 294 | artist_names: string | null; 295 | dimensions: dimensionsType | null; 296 | image: ImageType | null; 297 | images: Array | null; 298 | /** Returns the associated Fair/Sale/PartnerShow */ 299 | context: ArtworkContextType | null; 300 | /** Returns the highlighted shows and articles */ 301 | highlights: Array | null; 302 | articles: Array | null; 303 | shows: Array | null; 304 | show: PartnerShowType | null; 305 | sale_artwork: SaleArtworkType | null; 306 | sale: SaleType | null; 307 | fair: FairType | null; 308 | edition_of: string | null; 309 | edition_sets: Array | null; 310 | description: string | null; 311 | exhibition_history: string | null; 312 | provenance: string | null; 313 | signature: string | null; 314 | additional_information: string | null; 315 | literature: string | null; 316 | publisher: string | null; 317 | manufacturer: string | null; 318 | series: string | null; 319 | meta: ArtworkMetaType | null; 320 | related: Array | null; 321 | layer: ArtworkLayerType | null; 322 | layers: Array | null; 323 | } 324 | 325 | /** 326 | description: null 327 | */ 328 | interface PartnerType { 329 | __typename: string; 330 | /** A globally unique ID. */ 331 | __id: string; 332 | /** A type-specific ID. */ 333 | id: string; 334 | /** A type-specific Gravity Mongo Document ID. */ 335 | _id: string; 336 | cached: number | null; 337 | name: string | null; 338 | collecting_institution: string | null; 339 | is_default_profile_public: boolean | null; 340 | has_fair_partnership: boolean | null; 341 | type: string | null; 342 | href: string | null; 343 | is_linkable: boolean | null; 344 | is_pre_qualify: boolean | null; 345 | initials: string | null; 346 | default_profile_id: string | null; 347 | profile: ProfileType | null; 348 | shows: Array | null; 349 | artworks: Array | null; 350 | locations: Array | null; 351 | contact_message: string | null; 352 | counts: PartnerCountsType | null; 353 | } 354 | 355 | /** 356 | description: null 357 | */ 358 | interface ProfileType { 359 | __typename: string; 360 | /** A globally unique ID. */ 361 | __id: string; 362 | /** A type-specific ID. */ 363 | id: string; 364 | /** A type-specific Gravity Mongo Document ID. */ 365 | _id: string; 366 | cached: number | null; 367 | name: string | null; 368 | image: ImageType | null; 369 | initials: string | null; 370 | icon: ImageType | null; 371 | href: string | null; 372 | is_published: boolean | null; 373 | bio: string | null; 374 | counts: ProfileCountsType | null; 375 | } 376 | 377 | /** 378 | description: null 379 | */ 380 | interface ProfileCountsType { 381 | __typename: string; 382 | follows: any | null; 383 | } 384 | 385 | /** 386 | description: null 387 | */ 388 | type PartnerShowSortsEnum = "created_at_asc" | "created_at_desc" | "end_at_asc" | "end_at_desc" | "start_at_asc" | "start_at_desc" | "name_asc" | "name_desc" | "publish_at_asc" | "publish_at_desc" | "CREATED_AT_ASC" | "CREATED_AT_DESC" | "END_AT_ASC" | "END_AT_DESC" | "START_AT_ASC" | "START_AT_DESC" | "NAME_ASC" | "NAME_DESC" | "PUBLISH_AT_ASC" | "PUBLISH_AT_DESC"; 389 | 390 | /** 391 | description: null 392 | */ 393 | type EventStatusEnum = "current" | "running" | "closed" | "upcoming" | "CURRENT" | "RUNNING" | "CLOSED" | "UPCOMING"; 394 | 395 | /** 396 | description: null 397 | */ 398 | interface NearType { 399 | lat: number; 400 | lng: number; 401 | max_distance?: number; 402 | } 403 | 404 | /** 405 | description: null 406 | */ 407 | interface PartnerShowType { 408 | __typename: string; 409 | /** A globally unique ID. */ 410 | __id: string; 411 | /** A type-specific ID. */ 412 | id: string; 413 | /** A type-specific Gravity Mongo Document ID. */ 414 | _id: string; 415 | cached: number | null; 416 | href: string | null; 417 | kind: string | null; 418 | /** The exhibition title */ 419 | name: string | null; 420 | description: string | null; 421 | type: string | null; 422 | displayable: boolean | null; 423 | /** Gravity doesn’t expose the `active` flag. Temporarily re-state its logic. */ 424 | is_active: boolean | null; 425 | is_displayable: boolean | null; 426 | is_fair_booth: boolean | null; 427 | press_release: string | null; 428 | start_at: string | null; 429 | end_at: string | null; 430 | /** A formatted description of the start to end dates */ 431 | exhibition_period: string | null; 432 | artists: Array | null; 433 | partner: PartnerType | null; 434 | fair: FairType | null; 435 | location: LocationType | null; 436 | status: string | null; 437 | /** A formatted update on upcoming status changes */ 438 | status_update: string | null; 439 | events: Array | null; 440 | counts: PartnerShowCountsType | null; 441 | artworks: Array | null; 442 | meta_image: ImageType | null; 443 | cover_image: ImageType | null; 444 | images: Array | null; 445 | } 446 | 447 | /** 448 | description: null 449 | */ 450 | type FormatEnum = "HTML" | "PLAIN" | "markdown"; 451 | 452 | /** 453 | description: null 454 | */ 455 | interface ArtistType { 456 | __typename: string; 457 | /** A globally unique ID. */ 458 | __id: string; 459 | /** A type-specific ID. */ 460 | id: string; 461 | /** A type-specific Gravity Mongo Document ID. */ 462 | _id: string; 463 | cached: number | null; 464 | href: string | null; 465 | /** Use this attribute to sort by when sorting a collection of Artists */ 466 | sortable_id: string | null; 467 | name: string | null; 468 | initials: string | null; 469 | gender: string | null; 470 | years: string | null; 471 | is_public: boolean | null; 472 | is_consignable: boolean | null; 473 | public: boolean | null; 474 | consignable: boolean | null; 475 | /** Only specific Artists should show a link to auction results. */ 476 | is_display_auction_link: boolean | null; 477 | display_auction_link: boolean | null; 478 | has_metadata: boolean | null; 479 | hometown: string | null; 480 | location: string | null; 481 | nationality: string | null; 482 | birthday: string | null; 483 | deathday: string | null; 484 | /** A string of the form "Nationality, Birthday (or Birthday-Deathday)" */ 485 | formatted_nationality_and_birthday: string | null; 486 | /** The Artist biography article written by Artsy */ 487 | biography: ArticleType | null; 488 | alternate_names: Array | null; 489 | meta: ArtistMetaType | null; 490 | blurb: string | null; 491 | biography_blurb: ArtistBlurbType | null; 492 | is_shareable: boolean | null; 493 | bio: string | null; 494 | counts: ArtistCountsType | null; 495 | artworks: Array | null; 496 | /** A string showing the total number of works and those for sale */ 497 | formatted_artworks_count: string | null; 498 | image: ImageType | null; 499 | artists: Array | null; 500 | contemporary: Array | null; 501 | carousel: ArtistCarouselType | null; 502 | statuses: ArtistStatusesType | null; 503 | /** Custom-sorted list of shows for an artist, in order of significance. */ 504 | exhibition_highlights: Array | null; 505 | partner_shows: Array | null; 506 | shows: Array | null; 507 | partner_artists: Array | null; 508 | sales: Array | null; 509 | articles: Array | null; 510 | } 511 | 512 | /** 513 | description: null 514 | */ 515 | interface ArtistMetaType { 516 | __typename: string; 517 | title: string | null; 518 | description: string | null; 519 | } 520 | 521 | /** 522 | description: null 523 | */ 524 | interface ArtistBlurbType { 525 | __typename: string; 526 | text: string | null; 527 | credit: string | null; 528 | /** The partner id of the partner who submitted the featured bio. */ 529 | partner_id: string | null; 530 | } 531 | 532 | /** 533 | description: null 534 | */ 535 | interface ArtistCountsType { 536 | __typename: string; 537 | artworks: any | null; 538 | follows: any | null; 539 | for_sale_artworks: any | null; 540 | partner_shows: any | null; 541 | related_artists: any | null; 542 | articles: any | null; 543 | } 544 | 545 | /** 546 | description: null 547 | */ 548 | type ArtworkSortsEnum = "title_asc" | "title_desc" | "created_at_asc" | "created_at_desc" | "deleted_at_asc" | "deleted_at_desc" | "iconicity_desc" | "merchandisability_desc" | "published_at_asc" | "published_at_desc" | "partner_updated_at_desc" | "availability_desc" | "TITLE_ASC" | "TITLE_DESC" | "CREATED_AT_ASC" | "CREATED_AT_DESC" | "DELETED_AT_ASC" | "DELETED_AT_DESC" | "ICONICITY_DESC" | "MERCHANDISABILITY_DESC" | "PUBLISHED_AT_ASC" | "PUBLISHED_AT_DESC" | "PARTNER_UPDATED_AT_DESC" | "AVAILABILITY_DESC"; 549 | 550 | /** 551 | description: null 552 | */ 553 | type ArtistArtworksFiltersEnum = "IS_FOR_SALE" | "IS_NOT_FOR_SALE"; 554 | 555 | /** 556 | description: null 557 | */ 558 | interface ArtistCarouselType { 559 | __typename: string; 560 | images: Array | null; 561 | } 562 | 563 | /** 564 | description: null 565 | */ 566 | interface ArtistStatusesType { 567 | __typename: string; 568 | artworks: boolean | null; 569 | shows: boolean | null; 570 | cv: boolean | null; 571 | artists: boolean | null; 572 | contemporary: boolean | null; 573 | articles: boolean | null; 574 | auction_lots: boolean | null; 575 | biography: boolean | null; 576 | } 577 | 578 | /** 579 | description: null 580 | */ 581 | interface ShowType { 582 | __typename: string; 583 | /** A globally unique ID. */ 584 | __id: string; 585 | /** A type-specific ID. */ 586 | id: string; 587 | /** A type-specific Gravity Mongo Document ID. */ 588 | _id: string; 589 | cached: number | null; 590 | href: string | null; 591 | kind: string | null; 592 | /** The exhibition title */ 593 | name: string | null; 594 | description: string | null; 595 | type: string | null; 596 | displayable: boolean | null; 597 | /** Gravity doesn’t expose the `active` flag. Temporarily re-state its logic. */ 598 | is_active: boolean | null; 599 | is_displayable: boolean | null; 600 | is_fair_booth: boolean | null; 601 | is_reference: boolean | null; 602 | press_release: string | null; 603 | start_at: string | null; 604 | end_at: string | null; 605 | /** A formatted description of the start to end dates */ 606 | exhibition_period: string | null; 607 | artists: Array | null; 608 | artists_without_artworks: Array | null; 609 | partner: PartnerTypesType | null; 610 | fair: FairType | null; 611 | location: LocationType | null; 612 | city: string | null; 613 | status: string | null; 614 | /** A formatted update on upcoming status changes */ 615 | status_update: string | null; 616 | events: Array | null; 617 | counts: ShowCountsType | null; 618 | artworks: Array | null; 619 | meta_image: ImageType | null; 620 | cover_image: ImageType | null; 621 | images: Array | null; 622 | } 623 | 624 | /** 625 | description: null 626 | */ 627 | type PartnerTypes = PartnerType | ExternalPartnerType; 628 | 629 | /** 630 | description: null 631 | */ 632 | interface PartnerTypesType { 633 | __typename: string; 634 | 635 | } 636 | 637 | /** 638 | description: null 639 | */ 640 | interface ExternalPartnerType { 641 | __typename: string; 642 | /** A globally unique ID. */ 643 | __id: string; 644 | /** A type-specific ID. */ 645 | id: string; 646 | name: string | null; 647 | city: string | null; 648 | } 649 | 650 | /** 651 | description: null 652 | */ 653 | interface FairType { 654 | __typename: string; 655 | /** A globally unique ID. */ 656 | __id: string; 657 | /** A type-specific ID. */ 658 | id: string; 659 | /** A type-specific Gravity Mongo Document ID. */ 660 | _id: string; 661 | cached: number | null; 662 | banner_size: string | null; 663 | profile: ProfileType | null; 664 | has_full_feature: boolean | null; 665 | has_homepage_section: boolean | null; 666 | has_listing: boolean | null; 667 | has_large_banner: boolean | null; 668 | href: string | null; 669 | image: ImageType | null; 670 | location: LocationType | null; 671 | /** Are we currently in the fair's active period? */ 672 | is_active: boolean | null; 673 | start_at: string | null; 674 | end_at: string | null; 675 | name: string | null; 676 | tagline: string | null; 677 | published: boolean | null; 678 | is_published: boolean | null; 679 | organizer: organizerType | null; 680 | } 681 | 682 | /** 683 | description: null 684 | */ 685 | interface LocationType { 686 | __typename: string; 687 | /** A globally unique ID. */ 688 | __id: string; 689 | /** A type-specific ID. */ 690 | id: string; 691 | cached: number | null; 692 | city: string | null; 693 | country: string | null; 694 | coordinates: coordinatesType | null; 695 | display: string | null; 696 | address: string | null; 697 | address_2: string | null; 698 | postal_code: string | null; 699 | state: string | null; 700 | phone: string | null; 701 | day_schedules: Array | null; 702 | } 703 | 704 | /** 705 | description: null 706 | */ 707 | interface coordinatesType { 708 | __typename: string; 709 | lat: number | null; 710 | lng: number | null; 711 | } 712 | 713 | /** 714 | description: null 715 | */ 716 | interface DayScheduleType { 717 | __typename: string; 718 | start_time: number | null; 719 | end_time: number | null; 720 | day_of_week: string | null; 721 | } 722 | 723 | /** 724 | description: null 725 | */ 726 | interface organizerType { 727 | __typename: string; 728 | profile_id: string | null; 729 | } 730 | 731 | /** 732 | description: null 733 | */ 734 | interface PartnerShowEventTypeType { 735 | __typename: string; 736 | title: string | null; 737 | description: string | null; 738 | event_type: string | null; 739 | start_at: string | null; 740 | end_at: string | null; 741 | } 742 | 743 | /** 744 | description: null 745 | */ 746 | interface ShowCountsType { 747 | __typename: string; 748 | artworks: number | null; 749 | eligible_artworks: any | null; 750 | } 751 | 752 | /** 753 | description: null 754 | */ 755 | interface PartnerArtistType { 756 | __typename: string; 757 | /** A globally unique ID. */ 758 | __id: string; 759 | /** A type-specific ID. */ 760 | id: string; 761 | counts: PartnerArtistCountsType | null; 762 | is_display_on_partner_profile: boolean | null; 763 | is_represented_by: boolean | null; 764 | sortable_id: string | null; 765 | is_use_default_biography: boolean | null; 766 | biography: string | null; 767 | partner: PartnerType | null; 768 | artist: ArtistType | null; 769 | } 770 | 771 | /** 772 | description: null 773 | */ 774 | interface PartnerArtistCountsType { 775 | __typename: string; 776 | artworks: any | null; 777 | for_sale_artworks: any | null; 778 | } 779 | 780 | /** 781 | description: null 782 | */ 783 | type SaleSortsEnum = "_ID_ASC" | "_ID_DESC" | "NAME_ASC" | "NAME_DESC" | "CREATED_AT_ASC" | "CREATED_AT_DESC" | "END_AT_ASC" | "END_AT_DESC" | "START_AT_ASC" | "START_AT_DESC" | "ELIGIBLE_SALE_ARTWORKS_COUNT_ASC" | "ELIGIBLE_SALE_ARTWORKS_COUNT_DESC" | "TIMELY_AT_NAME_ASC" | "TIMELY_AT_NAME_DESC"; 784 | 785 | /** 786 | description: null 787 | */ 788 | interface SaleType { 789 | __typename: string; 790 | /** A globally unique ID. */ 791 | __id: string; 792 | /** A type-specific ID. */ 793 | id: string; 794 | /** A type-specific Gravity Mongo Document ID. */ 795 | _id: string; 796 | cached: number | null; 797 | name: string | null; 798 | href: string | null; 799 | description: string | null; 800 | sale_type: string | null; 801 | is_auction: boolean | null; 802 | is_auction_promo: boolean | null; 803 | is_preview: boolean | null; 804 | is_open: boolean | null; 805 | is_live_open: boolean | null; 806 | is_closed: boolean | null; 807 | is_with_buyers_premium: boolean | null; 808 | auction_state: string | null; 809 | status: string | null; 810 | registration_ends_at: string | null; 811 | start_at: string | null; 812 | end_at: string | null; 813 | live_start_at: string | null; 814 | event_start_at: string | null; 815 | event_end_at: string | null; 816 | currency: string | null; 817 | sale_artworks: Array | null; 818 | artworks: Array | null; 819 | cover_image: ImageType | null; 820 | sale_artwork: SaleArtworkType | null; 821 | profile: ProfileType | null; 822 | /** A bid increment policy that explains minimum bids in ranges. */ 823 | bid_increments: Array | null; 824 | /** Auction's buyer's premium policy. */ 825 | buyers_premium: Array | null; 826 | } 827 | 828 | /** 829 | description: null 830 | */ 831 | interface SaleArtworkType { 832 | __typename: string; 833 | /** A globally unique ID. */ 834 | __id: string; 835 | /** A type-specific ID. */ 836 | id: string; 837 | /** A type-specific Gravity Mongo Document ID. */ 838 | _id: string; 839 | cached: number | null; 840 | sale_id: string | null; 841 | sale: SaleType | null; 842 | position: number | null; 843 | lot_number: string | null; 844 | /** Currency abbreviation (e.g. "USD") */ 845 | currency: string | null; 846 | /** Currency symbol (e.g. "$") */ 847 | symbol: string | null; 848 | reserve_status: string | null; 849 | is_with_reserve: boolean | null; 850 | is_bid_on: boolean | null; 851 | /** Can bids be placed on the artwork? */ 852 | is_biddable: boolean | null; 853 | reserve_message: string | null; 854 | reserve: SaleArtworkReserveType | null; 855 | low_estimate: SaleArtworkLowEstimateType | null; 856 | high_estimate: SaleArtworkHighEstimateType | null; 857 | opening_bid: SaleArtworkOpeningBidType | null; 858 | minimum_next_bid: SaleArtworkMinimumNextBidType | null; 859 | current_bid: SaleArtworkCurrentBidType | null; 860 | highest_bid: SaleArtworkHighestBidType | null; 861 | artwork: ArtworkType | null; 862 | estimate: string | null; 863 | counts: SaleArtworkCountsType | null; 864 | /** Singular estimate field, if specified */ 865 | estimate_cents: number | null; 866 | low_estimate_cents: number | null; 867 | high_estimate_cents: number | null; 868 | opening_bid_cents: number | null; 869 | minimum_next_bid_cents: number | null; 870 | bidder_positions_count: number | null; 871 | bid_increments: Array | null; 872 | } 873 | 874 | /** 875 | description: null 876 | */ 877 | interface SaleArtworkReserveType { 878 | __typename: string; 879 | /** An amount of money expressed in cents. */ 880 | cents: number | null; 881 | /** A pre-formatted price. */ 882 | display: string | null; 883 | /** A formatted price with various currency formatting options. */ 884 | amount: string | null; 885 | } 886 | 887 | /** 888 | description: null 889 | */ 890 | interface SaleArtworkLowEstimateType { 891 | __typename: string; 892 | /** An amount of money expressed in cents. */ 893 | cents: number | null; 894 | /** A pre-formatted price. */ 895 | display: string | null; 896 | /** A formatted price with various currency formatting options. */ 897 | amount: string | null; 898 | } 899 | 900 | /** 901 | description: null 902 | */ 903 | interface SaleArtworkHighEstimateType { 904 | __typename: string; 905 | /** An amount of money expressed in cents. */ 906 | cents: number | null; 907 | /** A pre-formatted price. */ 908 | display: string | null; 909 | /** A formatted price with various currency formatting options. */ 910 | amount: string | null; 911 | } 912 | 913 | /** 914 | description: null 915 | */ 916 | interface SaleArtworkOpeningBidType { 917 | __typename: string; 918 | /** An amount of money expressed in cents. */ 919 | cents: number | null; 920 | /** A pre-formatted price. */ 921 | display: string | null; 922 | /** A formatted price with various currency formatting options. */ 923 | amount: string | null; 924 | } 925 | 926 | /** 927 | description: null 928 | */ 929 | interface SaleArtworkMinimumNextBidType { 930 | __typename: string; 931 | /** An amount of money expressed in cents. */ 932 | cents: number | null; 933 | /** A pre-formatted price. */ 934 | display: string | null; 935 | /** A formatted price with various currency formatting options. */ 936 | amount: string | null; 937 | } 938 | 939 | /** 940 | description: null 941 | */ 942 | interface SaleArtworkCurrentBidType { 943 | __typename: string; 944 | /** An amount of money expressed in cents. */ 945 | cents: number | null; 946 | /** A pre-formatted price. */ 947 | display: string | null; 948 | /** A formatted price with various currency formatting options. */ 949 | amount: string | null; 950 | } 951 | 952 | /** 953 | description: null 954 | */ 955 | interface SaleArtworkHighestBidType { 956 | __typename: string; 957 | id: string | null; 958 | created_at: string | null; 959 | is_cancelled: boolean | null; 960 | /** A formatted price with various currency formatting options. */ 961 | amount: string | null; 962 | cents: number | null; 963 | display: string | null; 964 | amount_cents: number | null; 965 | } 966 | 967 | /** 968 | description: null 969 | */ 970 | interface SaleArtworkCountsType { 971 | __typename: string; 972 | bidder_positions: any | null; 973 | } 974 | 975 | /** 976 | description: null 977 | */ 978 | interface BidIncrementType { 979 | __typename: string; 980 | from: number | null; 981 | to: number | null; 982 | amount: number | null; 983 | } 984 | 985 | /** 986 | description: null 987 | */ 988 | interface BuyersPremiumType { 989 | __typename: string; 990 | /** A formatted price with various currency formatting options. */ 991 | amount: string | null; 992 | cents: number | null; 993 | percent: number | null; 994 | } 995 | 996 | /** 997 | description: null 998 | */ 999 | interface PartnerShowCountsType { 1000 | __typename: string; 1001 | artworks: number | null; 1002 | eligible_artworks: any | null; 1003 | } 1004 | 1005 | /** 1006 | description: null 1007 | */ 1008 | interface PartnerCountsType { 1009 | __typename: string; 1010 | artworks: any | null; 1011 | artists: any | null; 1012 | partner_artists: any | null; 1013 | eligible_artworks: any | null; 1014 | published_for_sale_artworks: any | null; 1015 | published_not_for_sale_artworks: any | null; 1016 | shows: any | null; 1017 | displayable_shows: any | null; 1018 | current_displayable_shows: any | null; 1019 | artist_documents: any | null; 1020 | partner_show_documents: any | null; 1021 | } 1022 | 1023 | /** 1024 | description: null 1025 | */ 1026 | interface dimensionsType { 1027 | __typename: string; 1028 | in: string | null; 1029 | cm: string | null; 1030 | } 1031 | 1032 | /** 1033 | description: null 1034 | */ 1035 | type ArtworkContext = ArtworkContextAuctionType | ArtworkContextSaleType | ArtworkContextFairType | ArtworkContextPartnerShowType; 1036 | 1037 | /** 1038 | description: null 1039 | */ 1040 | interface ArtworkContextType { 1041 | __typename: string; 1042 | 1043 | } 1044 | 1045 | /** 1046 | description: null 1047 | */ 1048 | interface ArtworkContextAuctionType { 1049 | __typename: string; 1050 | /** A globally unique ID. */ 1051 | __id: string; 1052 | /** A type-specific ID. */ 1053 | id: string; 1054 | /** A type-specific Gravity Mongo Document ID. */ 1055 | _id: string; 1056 | cached: number | null; 1057 | name: string | null; 1058 | href: string | null; 1059 | description: string | null; 1060 | sale_type: string | null; 1061 | is_auction: boolean | null; 1062 | is_auction_promo: boolean | null; 1063 | is_preview: boolean | null; 1064 | is_open: boolean | null; 1065 | is_live_open: boolean | null; 1066 | is_closed: boolean | null; 1067 | is_with_buyers_premium: boolean | null; 1068 | auction_state: string | null; 1069 | status: string | null; 1070 | registration_ends_at: string | null; 1071 | start_at: string | null; 1072 | end_at: string | null; 1073 | live_start_at: string | null; 1074 | event_start_at: string | null; 1075 | event_end_at: string | null; 1076 | currency: string | null; 1077 | sale_artworks: Array | null; 1078 | artworks: Array | null; 1079 | cover_image: ImageType | null; 1080 | sale_artwork: SaleArtworkType | null; 1081 | profile: ProfileType | null; 1082 | /** A bid increment policy that explains minimum bids in ranges. */ 1083 | bid_increments: Array | null; 1084 | /** Auction's buyer's premium policy. */ 1085 | buyers_premium: Array | null; 1086 | } 1087 | 1088 | /** 1089 | description: null 1090 | */ 1091 | interface ArtworkContextSaleType { 1092 | __typename: string; 1093 | /** A globally unique ID. */ 1094 | __id: string; 1095 | /** A type-specific ID. */ 1096 | id: string; 1097 | /** A type-specific Gravity Mongo Document ID. */ 1098 | _id: string; 1099 | cached: number | null; 1100 | name: string | null; 1101 | href: string | null; 1102 | description: string | null; 1103 | sale_type: string | null; 1104 | is_auction: boolean | null; 1105 | is_auction_promo: boolean | null; 1106 | is_preview: boolean | null; 1107 | is_open: boolean | null; 1108 | is_live_open: boolean | null; 1109 | is_closed: boolean | null; 1110 | is_with_buyers_premium: boolean | null; 1111 | auction_state: string | null; 1112 | status: string | null; 1113 | registration_ends_at: string | null; 1114 | start_at: string | null; 1115 | end_at: string | null; 1116 | live_start_at: string | null; 1117 | event_start_at: string | null; 1118 | event_end_at: string | null; 1119 | currency: string | null; 1120 | sale_artworks: Array | null; 1121 | artworks: Array | null; 1122 | cover_image: ImageType | null; 1123 | sale_artwork: SaleArtworkType | null; 1124 | profile: ProfileType | null; 1125 | /** A bid increment policy that explains minimum bids in ranges. */ 1126 | bid_increments: Array | null; 1127 | /** Auction's buyer's premium policy. */ 1128 | buyers_premium: Array | null; 1129 | } 1130 | 1131 | /** 1132 | description: null 1133 | */ 1134 | interface ArtworkContextFairType { 1135 | __typename: string; 1136 | /** A globally unique ID. */ 1137 | __id: string; 1138 | /** A type-specific ID. */ 1139 | id: string; 1140 | /** A type-specific Gravity Mongo Document ID. */ 1141 | _id: string; 1142 | cached: number | null; 1143 | banner_size: string | null; 1144 | profile: ProfileType | null; 1145 | has_full_feature: boolean | null; 1146 | has_homepage_section: boolean | null; 1147 | has_listing: boolean | null; 1148 | has_large_banner: boolean | null; 1149 | href: string | null; 1150 | image: ImageType | null; 1151 | location: LocationType | null; 1152 | /** Are we currently in the fair's active period? */ 1153 | is_active: boolean | null; 1154 | start_at: string | null; 1155 | end_at: string | null; 1156 | name: string | null; 1157 | tagline: string | null; 1158 | published: boolean | null; 1159 | is_published: boolean | null; 1160 | organizer: organizerType | null; 1161 | } 1162 | 1163 | /** 1164 | description: null 1165 | */ 1166 | interface ArtworkContextPartnerShowType { 1167 | __typename: string; 1168 | /** A globally unique ID. */ 1169 | __id: string; 1170 | /** A type-specific ID. */ 1171 | id: string; 1172 | /** A type-specific Gravity Mongo Document ID. */ 1173 | _id: string; 1174 | cached: number | null; 1175 | href: string | null; 1176 | kind: string | null; 1177 | /** The exhibition title */ 1178 | name: string | null; 1179 | description: string | null; 1180 | type: string | null; 1181 | displayable: boolean | null; 1182 | /** Gravity doesn’t expose the `active` flag. Temporarily re-state its logic. */ 1183 | is_active: boolean | null; 1184 | is_displayable: boolean | null; 1185 | is_fair_booth: boolean | null; 1186 | press_release: string | null; 1187 | start_at: string | null; 1188 | end_at: string | null; 1189 | /** A formatted description of the start to end dates */ 1190 | exhibition_period: string | null; 1191 | artists: Array | null; 1192 | partner: PartnerType | null; 1193 | fair: FairType | null; 1194 | location: LocationType | null; 1195 | status: string | null; 1196 | /** A formatted update on upcoming status changes */ 1197 | status_update: string | null; 1198 | events: Array | null; 1199 | counts: PartnerShowCountsType | null; 1200 | artworks: Array | null; 1201 | meta_image: ImageType | null; 1202 | cover_image: ImageType | null; 1203 | images: Array | null; 1204 | } 1205 | 1206 | /** 1207 | description: null 1208 | */ 1209 | type Highlighted = HighlightedShowType | HighlightedArticleType; 1210 | 1211 | /** 1212 | description: null 1213 | */ 1214 | interface HighlightedType { 1215 | __typename: string; 1216 | 1217 | } 1218 | 1219 | /** 1220 | description: null 1221 | */ 1222 | interface HighlightedShowType { 1223 | __typename: string; 1224 | /** A globally unique ID. */ 1225 | __id: string; 1226 | /** A type-specific ID. */ 1227 | id: string; 1228 | /** A type-specific Gravity Mongo Document ID. */ 1229 | _id: string; 1230 | cached: number | null; 1231 | href: string | null; 1232 | kind: string | null; 1233 | /** The exhibition title */ 1234 | name: string | null; 1235 | description: string | null; 1236 | type: string | null; 1237 | displayable: boolean | null; 1238 | /** Gravity doesn’t expose the `active` flag. Temporarily re-state its logic. */ 1239 | is_active: boolean | null; 1240 | is_displayable: boolean | null; 1241 | is_fair_booth: boolean | null; 1242 | press_release: string | null; 1243 | start_at: string | null; 1244 | end_at: string | null; 1245 | /** A formatted description of the start to end dates */ 1246 | exhibition_period: string | null; 1247 | artists: Array | null; 1248 | partner: PartnerType | null; 1249 | fair: FairType | null; 1250 | location: LocationType | null; 1251 | status: string | null; 1252 | /** A formatted update on upcoming status changes */ 1253 | status_update: string | null; 1254 | events: Array | null; 1255 | counts: PartnerShowCountsType | null; 1256 | artworks: Array | null; 1257 | meta_image: ImageType | null; 1258 | cover_image: ImageType | null; 1259 | images: Array | null; 1260 | } 1261 | 1262 | /** 1263 | description: null 1264 | */ 1265 | interface HighlightedArticleType { 1266 | __typename: string; 1267 | /** A globally unique ID. */ 1268 | __id: string; 1269 | /** A type-specific ID. */ 1270 | id: string; 1271 | cached: number | null; 1272 | title: string | null; 1273 | published_at: string | null; 1274 | updated_at: string | null; 1275 | thumbnail_title: string | null; 1276 | thumbnail_teaser: string | null; 1277 | author: AuthorType | null; 1278 | thumbnail_image: ImageType | null; 1279 | slug: string | null; 1280 | href: string | null; 1281 | } 1282 | 1283 | /** 1284 | description: null 1285 | */ 1286 | interface EditionSetType { 1287 | __typename: string; 1288 | /** A globally unique ID. */ 1289 | __id: string; 1290 | /** A type-specific ID. */ 1291 | id: string; 1292 | dimensions: dimensionsType | null; 1293 | edition_of: string | null; 1294 | is_acquireable: boolean | null; 1295 | is_sold: boolean | null; 1296 | is_for_sale: boolean | null; 1297 | price: string | null; 1298 | } 1299 | 1300 | /** 1301 | description: null 1302 | */ 1303 | interface ArtworkMetaType { 1304 | __typename: string; 1305 | title: string | null; 1306 | description: string | null; 1307 | image: string | null; 1308 | } 1309 | 1310 | /** 1311 | description: null 1312 | */ 1313 | interface ArtworkLayerType { 1314 | __typename: string; 1315 | /** A globally unique ID. */ 1316 | __id: string; 1317 | /** A type-specific ID. */ 1318 | id: string; 1319 | type: string | null; 1320 | name: string | null; 1321 | href: string | null; 1322 | description: string | null; 1323 | artworks: Array | null; 1324 | } 1325 | 1326 | /** 1327 | description: null 1328 | */ 1329 | type ArtistSortsEnum = "sortable_id_asc" | "sortable_id_desc" | "trending_desc" | "SORTABLE_ID_ASC" | "SORTABLE_ID_DESC" | "TRENDING_DESC"; 1330 | 1331 | /** 1332 | description: null 1333 | */ 1334 | type FairSortsEnum = "CREATED_AT_ASC" | "CREATED_AT_DESC" | "START_AT_ASC" | "START_AT_DESC" | "NAME_ASC" | "NAME_DESC"; 1335 | 1336 | /** 1337 | description: null 1338 | */ 1339 | interface GeneType { 1340 | __typename: string; 1341 | /** A globally unique ID. */ 1342 | __id: string; 1343 | /** A type-specific ID. */ 1344 | id: string; 1345 | /** A type-specific Gravity Mongo Document ID. */ 1346 | _id: string; 1347 | cached: number | null; 1348 | /** Artworks Elastic Search results */ 1349 | filtered_artworks: FilterArtworksType | null; 1350 | href: string | null; 1351 | name: string | null; 1352 | description: string | null; 1353 | image: ImageType | null; 1354 | artists: Array | null; 1355 | trending_artists: Array | null; 1356 | } 1357 | 1358 | /** 1359 | description: null 1360 | */ 1361 | type ArtworkAggregationEnum = "PRICE_RANGE" | "DIMENSION_RANGE" | "COLOR" | "PERIOD" | "MAJOR_PERIOD" | "PARTNER_CITY" | "MEDIUM" | "GALLERY" | "INSTITUTION" | "TOTAL" | "FOLLOWED_ARTISTS" | "MERCHANDISABLE_ARTISTS"; 1362 | 1363 | /** 1364 | description: null 1365 | */ 1366 | interface FilterArtworksType { 1367 | __typename: string; 1368 | /** Artwork results. */ 1369 | hits: Array | null; 1370 | total: number | null; 1371 | followed_artists_total: number | null; 1372 | counts: FilterArtworksCountsType | null; 1373 | /** Returns a list of merchandisable artists sorted by merch score. */ 1374 | merchandisable_artists: Array | null; 1375 | /** Returns aggregation counts for the given filter query. */ 1376 | aggregations: Array | null; 1377 | } 1378 | 1379 | /** 1380 | description: null 1381 | */ 1382 | interface FilterArtworksCountsType { 1383 | __typename: string; 1384 | total: any | null; 1385 | followed_artists: any | null; 1386 | } 1387 | 1388 | /** 1389 | description: The results for one of the requested aggregations 1390 | */ 1391 | interface ArtworksAggregationResultsType { 1392 | __typename: string; 1393 | slice: ArtworkAggregationEnum | null; 1394 | counts: Array | null; 1395 | } 1396 | 1397 | /** 1398 | description: One item in an aggregation 1399 | */ 1400 | interface AggregationCountType { 1401 | __typename: string; 1402 | /** A globally unique ID. */ 1403 | __id: string; 1404 | /** A type-specific ID. */ 1405 | id: string; 1406 | name: string | null; 1407 | count: number | null; 1408 | } 1409 | 1410 | /** 1411 | description: null 1412 | */ 1413 | interface HomePageType { 1414 | __typename: string; 1415 | /** Single artwork module to show on the home screen */ 1416 | artwork_module: HomePageArtworkModuleType | null; 1417 | /** Artwork modules to show on the home screen */ 1418 | artwork_modules: Array | null; 1419 | /** Single artist module to show on the home screen. */ 1420 | artist_module: HomePageArtistModuleType | null; 1421 | /** Artist modules to show on the home screen */ 1422 | artist_modules: Array | null; 1423 | /** A list of enabled hero units to show on the requested platform */ 1424 | hero_units: Array | null; 1425 | } 1426 | 1427 | /** 1428 | description: null 1429 | */ 1430 | interface HomePageArtworkModuleType { 1431 | __typename: string; 1432 | /** A globally unique ID. */ 1433 | __id: string; 1434 | key: string | null; 1435 | display: string | null; 1436 | is_displayable: boolean | null; 1437 | params: HomePageModulesParamsType | null; 1438 | context: HomePageModuleContextType | null; 1439 | title: string | null; 1440 | results: Array | null; 1441 | } 1442 | 1443 | /** 1444 | description: null 1445 | */ 1446 | interface HomePageModulesParamsType { 1447 | __typename: string; 1448 | gene_id: string | null; 1449 | medium: string | null; 1450 | price_range: string | null; 1451 | id: string | null; 1452 | followed_artist_id: string | null; 1453 | related_artist_id: string | null; 1454 | } 1455 | 1456 | /** 1457 | description: null 1458 | */ 1459 | type HomePageModuleContext = HomePageModuleContextFairType | HomePageModuleContextSaleType | HomePageModuleContextGeneType | HomePageModuleContextTrendingType | HomePageModuleContextFollowArtistsType | HomePageModuleContextRelatedArtistType | HomePageModuleContextFollowedArtistType; 1460 | 1461 | /** 1462 | description: null 1463 | */ 1464 | interface HomePageModuleContextType { 1465 | __typename: string; 1466 | 1467 | } 1468 | 1469 | /** 1470 | description: null 1471 | */ 1472 | interface HomePageModuleContextFairType { 1473 | __typename: string; 1474 | /** A globally unique ID. */ 1475 | __id: string; 1476 | /** A type-specific ID. */ 1477 | id: string; 1478 | /** A type-specific Gravity Mongo Document ID. */ 1479 | _id: string; 1480 | cached: number | null; 1481 | banner_size: string | null; 1482 | profile: ProfileType | null; 1483 | has_full_feature: boolean | null; 1484 | has_homepage_section: boolean | null; 1485 | has_listing: boolean | null; 1486 | has_large_banner: boolean | null; 1487 | href: string | null; 1488 | image: ImageType | null; 1489 | location: LocationType | null; 1490 | /** Are we currently in the fair's active period? */ 1491 | is_active: boolean | null; 1492 | start_at: string | null; 1493 | end_at: string | null; 1494 | name: string | null; 1495 | tagline: string | null; 1496 | published: boolean | null; 1497 | is_published: boolean | null; 1498 | organizer: organizerType | null; 1499 | } 1500 | 1501 | /** 1502 | description: null 1503 | */ 1504 | interface HomePageModuleContextSaleType { 1505 | __typename: string; 1506 | /** A globally unique ID. */ 1507 | __id: string; 1508 | /** A type-specific ID. */ 1509 | id: string; 1510 | /** A type-specific Gravity Mongo Document ID. */ 1511 | _id: string; 1512 | cached: number | null; 1513 | name: string | null; 1514 | href: string | null; 1515 | description: string | null; 1516 | sale_type: string | null; 1517 | is_auction: boolean | null; 1518 | is_auction_promo: boolean | null; 1519 | is_preview: boolean | null; 1520 | is_open: boolean | null; 1521 | is_live_open: boolean | null; 1522 | is_closed: boolean | null; 1523 | is_with_buyers_premium: boolean | null; 1524 | auction_state: string | null; 1525 | status: string | null; 1526 | registration_ends_at: string | null; 1527 | start_at: string | null; 1528 | end_at: string | null; 1529 | live_start_at: string | null; 1530 | event_start_at: string | null; 1531 | event_end_at: string | null; 1532 | currency: string | null; 1533 | sale_artworks: Array | null; 1534 | artworks: Array | null; 1535 | cover_image: ImageType | null; 1536 | sale_artwork: SaleArtworkType | null; 1537 | profile: ProfileType | null; 1538 | /** A bid increment policy that explains minimum bids in ranges. */ 1539 | bid_increments: Array | null; 1540 | /** Auction's buyer's premium policy. */ 1541 | buyers_premium: Array | null; 1542 | } 1543 | 1544 | /** 1545 | description: null 1546 | */ 1547 | interface HomePageModuleContextGeneType { 1548 | __typename: string; 1549 | /** A globally unique ID. */ 1550 | __id: string; 1551 | /** A type-specific ID. */ 1552 | id: string; 1553 | /** A type-specific Gravity Mongo Document ID. */ 1554 | _id: string; 1555 | cached: number | null; 1556 | /** Artworks Elastic Search results */ 1557 | filtered_artworks: FilterArtworksType | null; 1558 | href: string | null; 1559 | name: string | null; 1560 | description: string | null; 1561 | image: ImageType | null; 1562 | artists: Array | null; 1563 | trending_artists: Array | null; 1564 | } 1565 | 1566 | /** 1567 | description: null 1568 | */ 1569 | interface HomePageModuleContextTrendingType { 1570 | __typename: string; 1571 | artists: Array | null; 1572 | } 1573 | 1574 | /** 1575 | description: null 1576 | */ 1577 | interface HomePageModuleContextFollowArtistsType { 1578 | __typename: string; 1579 | artists: Array | null; 1580 | counts: FollowArtistCountsType | null; 1581 | } 1582 | 1583 | /** 1584 | description: null 1585 | */ 1586 | interface FollowArtistCountsType { 1587 | __typename: string; 1588 | artists: number | null; 1589 | } 1590 | 1591 | /** 1592 | description: null 1593 | */ 1594 | interface HomePageModuleContextRelatedArtistType { 1595 | __typename: string; 1596 | artist: ArtistType | null; 1597 | based_on: ArtistType | null; 1598 | } 1599 | 1600 | /** 1601 | description: null 1602 | */ 1603 | interface HomePageModuleContextFollowedArtistType { 1604 | __typename: string; 1605 | artist: ArtistType | null; 1606 | } 1607 | 1608 | /** 1609 | description: null 1610 | */ 1611 | type HomePageArtworkModuleTypesEnum = "ACTIVE_BIDS" | "FOLLOWED_ARTISTS" | "FOLLOWED_GALLERIES" | "SAVED_WORKS" | "RECOMMENDED_WORKS" | "LIVE_AUCTIONS" | "CURRENT_FAIRS" | "FOLLOWED_ARTIST" | "RELATED_ARTISTS" | "FOLLOWED_GENES" | "GENERIC_GENES"; 1612 | 1613 | /** 1614 | description: null 1615 | */ 1616 | type HomePageArtistModuleTypesEnum = "SUGGESTED" | "TRENDING" | "POPULAR"; 1617 | 1618 | /** 1619 | description: null 1620 | */ 1621 | interface HomePageArtistModuleType { 1622 | __typename: string; 1623 | /** A globally unique ID. */ 1624 | __id: string; 1625 | /** Module identifier. */ 1626 | key: string | null; 1627 | results: Array | null; 1628 | } 1629 | 1630 | /** 1631 | description: null 1632 | */ 1633 | type HomePageHeroUnitPlatformEnum = "MOBILE" | "DESKTOP" | "MARTSY"; 1634 | 1635 | /** 1636 | description: null 1637 | */ 1638 | interface HomePageHeroUnitType { 1639 | __typename: string; 1640 | /** A globally unique ID. */ 1641 | __id: string; 1642 | /** A type-specific ID. */ 1643 | id: string; 1644 | /** A type-specific Gravity Mongo Document ID. */ 1645 | _id: string; 1646 | cached: number | null; 1647 | heading: string | null; 1648 | href: string | null; 1649 | title: string | null; 1650 | /** The image to show, on desktop this defaults to the wide version. */ 1651 | background_image_url: string | null; 1652 | } 1653 | 1654 | /** 1655 | description: null 1656 | */ 1657 | type HomePageHeroUnitImageVersionEnum = "WIDE" | "NARROW"; 1658 | 1659 | /** 1660 | description: null 1661 | */ 1662 | interface OrderedSetType { 1663 | __typename: string; 1664 | /** A globally unique ID. */ 1665 | __id: string; 1666 | /** A type-specific ID. */ 1667 | id: string; 1668 | cached: number | null; 1669 | key: string | null; 1670 | name: string | null; 1671 | description: string | null; 1672 | item_type: string | null; 1673 | items: Array | null; 1674 | } 1675 | 1676 | /** 1677 | description: null 1678 | */ 1679 | type Item = ArtistItemType | FeaturedLinkItemType | GeneItemType; 1680 | 1681 | /** 1682 | description: null 1683 | */ 1684 | interface ItemType { 1685 | __typename: string; 1686 | 1687 | } 1688 | 1689 | /** 1690 | description: null 1691 | */ 1692 | interface ArtistItemType { 1693 | __typename: string; 1694 | /** A globally unique ID. */ 1695 | __id: string; 1696 | /** A type-specific ID. */ 1697 | id: string; 1698 | /** A type-specific Gravity Mongo Document ID. */ 1699 | _id: string; 1700 | cached: number | null; 1701 | href: string | null; 1702 | /** Use this attribute to sort by when sorting a collection of Artists */ 1703 | sortable_id: string | null; 1704 | name: string | null; 1705 | initials: string | null; 1706 | gender: string | null; 1707 | years: string | null; 1708 | is_public: boolean | null; 1709 | is_consignable: boolean | null; 1710 | public: boolean | null; 1711 | consignable: boolean | null; 1712 | /** Only specific Artists should show a link to auction results. */ 1713 | is_display_auction_link: boolean | null; 1714 | display_auction_link: boolean | null; 1715 | has_metadata: boolean | null; 1716 | hometown: string | null; 1717 | location: string | null; 1718 | nationality: string | null; 1719 | birthday: string | null; 1720 | deathday: string | null; 1721 | /** A string of the form "Nationality, Birthday (or Birthday-Deathday)" */ 1722 | formatted_nationality_and_birthday: string | null; 1723 | /** The Artist biography article written by Artsy */ 1724 | biography: ArticleType | null; 1725 | alternate_names: Array | null; 1726 | meta: ArtistMetaType | null; 1727 | blurb: string | null; 1728 | biography_blurb: ArtistBlurbType | null; 1729 | is_shareable: boolean | null; 1730 | bio: string | null; 1731 | counts: ArtistCountsType | null; 1732 | artworks: Array | null; 1733 | /** A string showing the total number of works and those for sale */ 1734 | formatted_artworks_count: string | null; 1735 | image: ImageType | null; 1736 | artists: Array | null; 1737 | contemporary: Array | null; 1738 | carousel: ArtistCarouselType | null; 1739 | statuses: ArtistStatusesType | null; 1740 | /** Custom-sorted list of shows for an artist, in order of significance. */ 1741 | exhibition_highlights: Array | null; 1742 | partner_shows: Array | null; 1743 | shows: Array | null; 1744 | partner_artists: Array | null; 1745 | sales: Array | null; 1746 | articles: Array | null; 1747 | } 1748 | 1749 | /** 1750 | description: null 1751 | */ 1752 | interface FeaturedLinkItemType { 1753 | __typename: string; 1754 | /** Attempt to get the ID of the entity of the FeaturedLink */ 1755 | id: string | null; 1756 | title: string | null; 1757 | initials: string | null; 1758 | subtitle: string | null; 1759 | href: string | null; 1760 | image: ImageType | null; 1761 | } 1762 | 1763 | /** 1764 | description: null 1765 | */ 1766 | interface GeneItemType { 1767 | __typename: string; 1768 | /** A globally unique ID. */ 1769 | __id: string; 1770 | /** A type-specific ID. */ 1771 | id: string; 1772 | /** A type-specific Gravity Mongo Document ID. */ 1773 | _id: string; 1774 | cached: number | null; 1775 | /** Artworks Elastic Search results */ 1776 | filtered_artworks: FilterArtworksType | null; 1777 | href: string | null; 1778 | name: string | null; 1779 | description: string | null; 1780 | image: ImageType | null; 1781 | artists: Array | null; 1782 | trending_artists: Array | null; 1783 | } 1784 | 1785 | /** 1786 | description: null 1787 | */ 1788 | type PartnersSortTypeEnum = "CREATED_AT_ASC" | "CREATED_AT_DESC" | "SORTABLE_ID_ASC" | "SORTABLE_ID_DESC" | "RELATIVE_SIZE_ASC" | "RELATIVE_SIZE_DESC" | "PUBLISHED_AT_DESC" | "RANDOM_SCORE_DESC"; 1789 | 1790 | /** 1791 | description: null 1792 | */ 1793 | type PartnerClassificationEnum = "AUCTION" | "DEMO" | "GALLERY" | "PRIVATE_COLLECTOR" | "PRIVATE_DEALER" | "INSTITUTION" | "INSTITUTIONAL_SELLER" | "BRAND"; 1794 | 1795 | /** 1796 | description: null 1797 | */ 1798 | type PartnersAggregationEnum = "LOCATION" | "CATEGORY" | "TOTAL"; 1799 | 1800 | /** 1801 | description: null 1802 | */ 1803 | interface FilterPartnersType { 1804 | __typename: string; 1805 | hits: Array | null; 1806 | total: number | null; 1807 | aggregations: Array | null; 1808 | } 1809 | 1810 | /** 1811 | description: The results for one of the requested aggregations 1812 | */ 1813 | interface PartnersAggregationResultsType { 1814 | __typename: string; 1815 | slice: PartnersAggregationEnum | null; 1816 | counts: Array | null; 1817 | } 1818 | 1819 | /** 1820 | description: null 1821 | */ 1822 | interface PartnerCategoryType { 1823 | __typename: string; 1824 | /** A globally unique ID. */ 1825 | __id: string; 1826 | /** A type-specific ID. */ 1827 | id: string; 1828 | cached: number | null; 1829 | name: string | null; 1830 | category_type: CategoryTypeEnum | null; 1831 | partners: Array | null; 1832 | } 1833 | 1834 | /** 1835 | description: null 1836 | */ 1837 | type CategoryTypeEnum = "GALLERY" | "INSTITUTION"; 1838 | 1839 | /** 1840 | description: null 1841 | */ 1842 | interface SearchType { 1843 | __typename: string; 1844 | cached: number | null; 1845 | total: number | null; 1846 | results: Array | null; 1847 | } 1848 | 1849 | /** 1850 | description: null 1851 | */ 1852 | interface SearchResultType { 1853 | __typename: string; 1854 | id: string | null; 1855 | title: string | null; 1856 | href: string | null; 1857 | snippet: string | null; 1858 | image: ImageType | null; 1859 | type: string | null; 1860 | entity: SearchEntityType | null; 1861 | } 1862 | 1863 | /** 1864 | description: null 1865 | */ 1866 | type SearchEntity = ArtistSearchEntityType | ArtworkSearchEntityType | ProfileSearchEntityType | PartnerShowSearchEntityType; 1867 | 1868 | /** 1869 | description: null 1870 | */ 1871 | interface SearchEntityType { 1872 | __typename: string; 1873 | 1874 | } 1875 | 1876 | /** 1877 | description: null 1878 | */ 1879 | interface ArtistSearchEntityType { 1880 | __typename: string; 1881 | /** A globally unique ID. */ 1882 | __id: string; 1883 | /** A type-specific ID. */ 1884 | id: string; 1885 | /** A type-specific Gravity Mongo Document ID. */ 1886 | _id: string; 1887 | cached: number | null; 1888 | href: string | null; 1889 | /** Use this attribute to sort by when sorting a collection of Artists */ 1890 | sortable_id: string | null; 1891 | name: string | null; 1892 | initials: string | null; 1893 | gender: string | null; 1894 | years: string | null; 1895 | is_public: boolean | null; 1896 | is_consignable: boolean | null; 1897 | public: boolean | null; 1898 | consignable: boolean | null; 1899 | /** Only specific Artists should show a link to auction results. */ 1900 | is_display_auction_link: boolean | null; 1901 | display_auction_link: boolean | null; 1902 | has_metadata: boolean | null; 1903 | hometown: string | null; 1904 | location: string | null; 1905 | nationality: string | null; 1906 | birthday: string | null; 1907 | deathday: string | null; 1908 | /** A string of the form "Nationality, Birthday (or Birthday-Deathday)" */ 1909 | formatted_nationality_and_birthday: string | null; 1910 | /** The Artist biography article written by Artsy */ 1911 | biography: ArticleType | null; 1912 | alternate_names: Array | null; 1913 | meta: ArtistMetaType | null; 1914 | blurb: string | null; 1915 | biography_blurb: ArtistBlurbType | null; 1916 | is_shareable: boolean | null; 1917 | bio: string | null; 1918 | counts: ArtistCountsType | null; 1919 | artworks: Array | null; 1920 | /** A string showing the total number of works and those for sale */ 1921 | formatted_artworks_count: string | null; 1922 | image: ImageType | null; 1923 | artists: Array | null; 1924 | contemporary: Array | null; 1925 | carousel: ArtistCarouselType | null; 1926 | statuses: ArtistStatusesType | null; 1927 | /** Custom-sorted list of shows for an artist, in order of significance. */ 1928 | exhibition_highlights: Array | null; 1929 | partner_shows: Array | null; 1930 | shows: Array | null; 1931 | partner_artists: Array | null; 1932 | sales: Array | null; 1933 | articles: Array | null; 1934 | } 1935 | 1936 | /** 1937 | description: null 1938 | */ 1939 | interface ArtworkSearchEntityType { 1940 | __typename: string; 1941 | /** A globally unique ID. */ 1942 | __id: string; 1943 | /** A type-specific ID. */ 1944 | id: string; 1945 | /** A type-specific Gravity Mongo Document ID. */ 1946 | _id: string; 1947 | cached: number | null; 1948 | to_s: string | null; 1949 | href: string | null; 1950 | title: string | null; 1951 | category: string | null; 1952 | medium: string | null; 1953 | date: string | null; 1954 | image_rights: string | null; 1955 | website: string | null; 1956 | collecting_institution: string | null; 1957 | partner: PartnerType | null; 1958 | /** Returns an HTML string representing the embedded content (video) */ 1959 | embed: string | null; 1960 | can_share_image: boolean | null; 1961 | is_embeddable_video: boolean | null; 1962 | is_shareable: boolean | null; 1963 | is_hangable: boolean | null; 1964 | /** True for inquireable artworks that have an exact price. */ 1965 | is_purchasable: boolean | null; 1966 | /** Do we want to encourage inquiries on this work? */ 1967 | is_inquireable: boolean | null; 1968 | /** Are we able to display a contact form on artwork pages? */ 1969 | is_contactable: boolean | null; 1970 | /** Is this artwork part of an auction? */ 1971 | is_in_auction: boolean | null; 1972 | /** Is this artwork part of a current show */ 1973 | is_in_show: boolean | null; 1974 | is_for_sale: boolean | null; 1975 | /** Is this artwork part of an auction that is currently running? */ 1976 | is_biddable: boolean | null; 1977 | is_unique: boolean | null; 1978 | is_sold: boolean | null; 1979 | is_ecommerce: boolean | null; 1980 | /** Whether work can be purchased through e-commerce */ 1981 | is_acquireable: boolean | null; 1982 | /** When in an auction, can the work be bought before any bids are placed */ 1983 | is_buy_nowable: boolean | null; 1984 | is_comparable_with_auction_results: boolean | null; 1985 | is_downloadable: boolean | null; 1986 | is_price_hidden: boolean | null; 1987 | is_price_range: boolean | null; 1988 | availability: string | null; 1989 | sale_message: string | null; 1990 | artist: ArtistType | null; 1991 | price: string | null; 1992 | contact_label: string | null; 1993 | cultural_maker: string | null; 1994 | artists: Array | null; 1995 | artist_names: string | null; 1996 | dimensions: dimensionsType | null; 1997 | image: ImageType | null; 1998 | images: Array | null; 1999 | /** Returns the associated Fair/Sale/PartnerShow */ 2000 | context: ArtworkContextType | null; 2001 | /** Returns the highlighted shows and articles */ 2002 | highlights: Array | null; 2003 | articles: Array | null; 2004 | shows: Array | null; 2005 | show: PartnerShowType | null; 2006 | sale_artwork: SaleArtworkType | null; 2007 | sale: SaleType | null; 2008 | fair: FairType | null; 2009 | edition_of: string | null; 2010 | edition_sets: Array | null; 2011 | description: string | null; 2012 | exhibition_history: string | null; 2013 | provenance: string | null; 2014 | signature: string | null; 2015 | additional_information: string | null; 2016 | literature: string | null; 2017 | publisher: string | null; 2018 | manufacturer: string | null; 2019 | series: string | null; 2020 | meta: ArtworkMetaType | null; 2021 | related: Array | null; 2022 | layer: ArtworkLayerType | null; 2023 | layers: Array | null; 2024 | } 2025 | 2026 | /** 2027 | description: null 2028 | */ 2029 | interface ProfileSearchEntityType { 2030 | __typename: string; 2031 | /** A globally unique ID. */ 2032 | __id: string; 2033 | /** A type-specific ID. */ 2034 | id: string; 2035 | /** A type-specific Gravity Mongo Document ID. */ 2036 | _id: string; 2037 | cached: number | null; 2038 | name: string | null; 2039 | image: ImageType | null; 2040 | initials: string | null; 2041 | icon: ImageType | null; 2042 | href: string | null; 2043 | is_published: boolean | null; 2044 | bio: string | null; 2045 | counts: ProfileCountsType | null; 2046 | } 2047 | 2048 | /** 2049 | description: null 2050 | */ 2051 | interface PartnerShowSearchEntityType { 2052 | __typename: string; 2053 | /** A globally unique ID. */ 2054 | __id: string; 2055 | /** A type-specific ID. */ 2056 | id: string; 2057 | /** A type-specific Gravity Mongo Document ID. */ 2058 | _id: string; 2059 | cached: number | null; 2060 | href: string | null; 2061 | kind: string | null; 2062 | /** The exhibition title */ 2063 | name: string | null; 2064 | description: string | null; 2065 | type: string | null; 2066 | displayable: boolean | null; 2067 | /** Gravity doesn’t expose the `active` flag. Temporarily re-state its logic. */ 2068 | is_active: boolean | null; 2069 | is_displayable: boolean | null; 2070 | is_fair_booth: boolean | null; 2071 | press_release: string | null; 2072 | start_at: string | null; 2073 | end_at: string | null; 2074 | /** A formatted description of the start to end dates */ 2075 | exhibition_period: string | null; 2076 | artists: Array | null; 2077 | partner: PartnerType | null; 2078 | fair: FairType | null; 2079 | location: LocationType | null; 2080 | status: string | null; 2081 | /** A formatted update on upcoming status changes */ 2082 | status_update: string | null; 2083 | events: Array | null; 2084 | counts: PartnerShowCountsType | null; 2085 | artworks: Array | null; 2086 | meta_image: ImageType | null; 2087 | cover_image: ImageType | null; 2088 | images: Array | null; 2089 | } 2090 | 2091 | /** 2092 | description: null 2093 | */ 2094 | type TrendingMetricsEnum = "ARTIST_FOLLOW" | "ARTIST_INQUIRY" | "ARTIST_SEARCH" | "ARTIST_SAVE" | "ARTIST_FAIR" | "ARTIST_AUCTION_LOT"; 2095 | 2096 | /** 2097 | description: null 2098 | */ 2099 | interface TrendingArtistsType { 2100 | __typename: string; 2101 | artists: Array | null; 2102 | } 2103 | 2104 | /** 2105 | description: null 2106 | */ 2107 | interface MeType { 2108 | __typename: string; 2109 | /** A globally unique ID. */ 2110 | __id: string; 2111 | /** A type-specific ID. */ 2112 | id: string; 2113 | type: string | null; 2114 | created_at: string | null; 2115 | email: string | null; 2116 | name: string | null; 2117 | paddle_number: string | null; 2118 | /** A list of the current user’s bidder registrations */ 2119 | bidders: Array | null; 2120 | /** The current user's status relating to bids on artworks */ 2121 | bidder_status: LotStandingType | null; 2122 | /** A list of the current user's bidder positions */ 2123 | bidder_positions: Array | null; 2124 | /** The current user's status relating to bids on artworks */ 2125 | lot_standing: LotStandingType | null; 2126 | /** A list of the current user's auction standings for given lots */ 2127 | lot_standings: Array | null; 2128 | sale_registrations: Array | null; 2129 | /** A list of the current user’s artist follows */ 2130 | follow_artists: FollowArtistsType | null; 2131 | /** A list of the current user’s suggested artists, based on a single artist */ 2132 | suggested_artists: Array | null; 2133 | } 2134 | 2135 | /** 2136 | description: null 2137 | */ 2138 | interface BidderType { 2139 | __typename: string; 2140 | /** A globally unique ID. */ 2141 | __id: string; 2142 | /** A type-specific ID. */ 2143 | id: string; 2144 | created_at: string | null; 2145 | pin: string | null; 2146 | sale: SaleType | null; 2147 | qualified_for_bidding: boolean | null; 2148 | } 2149 | 2150 | /** 2151 | description: null 2152 | */ 2153 | interface LotStandingType { 2154 | __typename: string; 2155 | bidder: BidderType | null; 2156 | sale_artwork: SaleArtworkType | null; 2157 | /** You are winning and reserve is met */ 2158 | is_highest_bidder: boolean | null; 2159 | /** You are the leading bidder without regard to reserve */ 2160 | is_leading_bidder: boolean | null; 2161 | /** Your bid if it is currently winning */ 2162 | active_bid: BidderPositionType | null; 2163 | /** Your most recent bid—which is not necessarily winning (may be higher or lower) */ 2164 | most_recent_bid: BidderPositionType | null; 2165 | } 2166 | 2167 | /** 2168 | description: null 2169 | */ 2170 | interface BidderPositionType { 2171 | __typename: string; 2172 | /** A globally unique ID. */ 2173 | __id: string; 2174 | /** A type-specific ID. */ 2175 | id: string; 2176 | created_at: string | null; 2177 | updated_at: string | null; 2178 | processed_at: string | null; 2179 | is_active: boolean | null; 2180 | is_retracted: boolean | null; 2181 | is_with_bid_max: boolean | null; 2182 | is_winning: boolean | null; 2183 | max_bid: BidderPositionMaxBidType | null; 2184 | suggested_next_bid: BidderPositionSuggestedNextBidType | null; 2185 | sale_artwork: SaleArtworkType | null; 2186 | highest_bid: HighestBidType | null; 2187 | display_max_bid_amount_dollars: string | null; 2188 | display_suggested_next_bid_dollars: string | null; 2189 | max_bid_amount_cents: number | null; 2190 | suggested_next_bid_cents: number | null; 2191 | } 2192 | 2193 | /** 2194 | description: null 2195 | */ 2196 | interface BidderPositionMaxBidType { 2197 | __typename: string; 2198 | /** An amount of money expressed in cents. */ 2199 | cents: number | null; 2200 | /** A pre-formatted price. */ 2201 | display: string | null; 2202 | /** A formatted price with various currency formatting options. */ 2203 | amount: string | null; 2204 | } 2205 | 2206 | /** 2207 | description: null 2208 | */ 2209 | interface BidderPositionSuggestedNextBidType { 2210 | __typename: string; 2211 | /** An amount of money expressed in cents. */ 2212 | cents: number | null; 2213 | /** A pre-formatted price. */ 2214 | display: string | null; 2215 | /** A formatted price with various currency formatting options. */ 2216 | amount: string | null; 2217 | } 2218 | 2219 | /** 2220 | description: null 2221 | */ 2222 | interface HighestBidType { 2223 | __typename: string; 2224 | /** A globally unique ID. */ 2225 | __id: string; 2226 | /** A type-specific ID. */ 2227 | id: string; 2228 | created_at: string | null; 2229 | number: number | null; 2230 | is_cancelled: boolean | null; 2231 | /** A formatted price with various currency formatting options. */ 2232 | amount: string | null; 2233 | cents: number | null; 2234 | display: string | null; 2235 | amount_cents: number | null; 2236 | display_amount_dollars: string | null; 2237 | } 2238 | 2239 | /** 2240 | description: null 2241 | */ 2242 | interface SaleRegistrationType { 2243 | __typename: string; 2244 | is_registered: boolean | null; 2245 | bidder: BidderType | null; 2246 | sale: SaleType | null; 2247 | } 2248 | 2249 | /** 2250 | description: null 2251 | */ 2252 | interface FollowArtistsType { 2253 | __typename: string; 2254 | artists: Array | null; 2255 | counts: FollowArtistCountsType | null; 2256 | } 2257 | 2258 | /** 2259 | description: null 2260 | */ 2261 | type RoleEnum = "PARTICIPANT" | "OPERATOR"; 2262 | } 2263 | -------------------------------------------------------------------------------- /app/relay/config.ts: -------------------------------------------------------------------------------- 1 | import * as Relay from "react-relay" 2 | 3 | export const metaphysicsURL = "https://metaphysics-staging.artsy.net" 4 | 5 | export function artsyNetworkLayer() { 6 | return new Relay.DefaultNetworkLayer(metaphysicsURL, { 7 | headers: { 8 | // 'X-USER-ID': Emission.userID, 9 | // 'X-ACCESS-TOKEN': Emission.authenticationToken, 10 | }, 11 | }) 12 | } 13 | 14 | /* 15 | * For the server. 16 | */ 17 | export function artsyRelayMiddleware(req: any, res: any, next: any) { 18 | res.locals.networkLayer = artsyNetworkLayer() 19 | next() 20 | } 21 | 22 | // TODO: Send to definitely typed? 23 | declare module "react-relay" { 24 | class Environment { 25 | injectNetworkLayer(networkLayer: RelayNetworkLayer): void 26 | } 27 | } 28 | 29 | /* 30 | * For the client. 31 | */ 32 | export function artsyRelayEnvironment() { 33 | const env: any = new Relay.Environment() 34 | env.injectNetworkLayer(artsyNetworkLayer()) 35 | return env 36 | } 37 | -------------------------------------------------------------------------------- /app/relay/root_queries.ts: -------------------------------------------------------------------------------- 1 | import * as Relay from "react-relay" 2 | 3 | export class ArtistQueryConfig extends Relay.Route { 4 | static queries = { 5 | artist: (component, params) => Relay.QL` 6 | query { 7 | artist(id: $artistID) { 8 | ${component.getFragment("artist", params)} 9 | } 10 | } 11 | `, 12 | } 13 | 14 | static paramDefinitions = { 15 | artistID: { required: true }, 16 | } 17 | 18 | static routeName = "ArtistQueryConfig" 19 | } 20 | -------------------------------------------------------------------------------- /app/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | import * as React from "react" 3 | import * as ReactDOMServer from "react-dom/server" 4 | import * as Relay from "react-relay" 5 | 6 | import IsomorphicRelay from "isomorphic-relay" 7 | 8 | import { artsyRelayMiddleware } from "./relay/config" 9 | import { ArtistQueryConfig } from "./relay/root_queries" 10 | 11 | import PureReactArtist from "./containers/pure-react/artist" 12 | import ReactInlineCSSArtist from "./containers/react-inline-css/artist" 13 | 14 | import * as ReactNative from "react-native-web" 15 | import ReactNativeWebArtist from "./containers/react-native-web/artist" 16 | 17 | import { StyleSheetServer } from "aphrodite" 18 | import ReactAphroditeArtist from "./containers/react-aphrodite/artist" 19 | 20 | import { SheetsRegistry, SheetsRegistryProvider } from "react-jss" 21 | import ReactJSSArtist from "./containers/react-jss/artist" 22 | 23 | const app = express.Router() 24 | 25 | app.use(artsyRelayMiddleware) 26 | 27 | app.get("/", (req: any, res: any, next: any) => { 28 | res.send(``) 35 | }) 36 | 37 | /* 38 | * PURE REACT/CSS 39 | * 40 | * [x] Server-side rendering 41 | * [x] Client-side rendering 42 | * [x] Client-side rehydration 43 | * [x] No limitation in CSS possibilities 44 | * [x] Styling cachable by client 45 | * [x] Small data size 46 | * [x] Vendor prefixes: possible with tooling such as PostCSS (autoprefixer) 47 | * [ ] Code+Style locality 48 | * [ ] Portability of mobile app code 49 | */ 50 | app.get("/pure-react/artist/:id", (req: any, res: any, next: any) => { 51 | IsomorphicRelay.prepareData({ 52 | Container: PureReactArtist, 53 | queryConfig: new ArtistQueryConfig({ artistID: req.params.id }), 54 | }, res.locals.networkLayer).then(({ data, props }) => { 55 | const content = ReactDOMServer.renderToString() 56 | res.send(` 57 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 |
${content}
68 | 69 | 70 | `) 71 | }).catch(next) 72 | }) 73 | 74 | app.get("/pure-react/style.css", (req: any, res: any, next: any) => { 75 | res.sendFile("./containers/pure-react/artist/style.css", { root: __dirname }) 76 | }) 77 | 78 | /* 79 | * REACT with builtin inline CSS support 80 | * 81 | * [x] Server-side rendering 82 | * [x] Client-side rendering 83 | * [x] Client-side rehydration 84 | * [ ] No limitation in CSS possibilities: e.g. keyframe animation and media queries don’t work inline 85 | * [ ] Styling cachable by client 86 | * [ ] Small data size: inline styles are repetitive and thus add byte size linearly 87 | * [-] Vendor prefixes: tooling should exist, but which to use for an isomorphic app wasn’t immediately clear. 88 | * [x] Code+Style locality 89 | * [-] Portability of mobile app code: no `StyleSheet` API and no: `` 90 | */ 91 | app.get("/react-inline-css/artist/:id", (req: any, res: any, next: any) => { 92 | IsomorphicRelay.prepareData({ 93 | Container: ReactInlineCSSArtist, 94 | queryConfig: new ArtistQueryConfig({ artistID: req.params.id }), 95 | }, res.locals.networkLayer).then(({ data, props }) => { 96 | const content = ReactDOMServer.renderToString() 97 | res.send(` 98 | 99 | 100 | 101 | 102 | 105 | 106 | 107 |
${content}
108 | 109 | 110 | `) 111 | }).catch(next) 112 | }) 113 | 114 | /* 115 | * REACT with aphrodite for inline CSS 116 | * 117 | * [x] Server-side rendering 118 | * [x] Client-side rendering 119 | * [x] Client-side rehydration 120 | * [x] No limitation in CSS possibilities: at least I believe media queries and keyframe animations are native? 121 | * [ ] Styling cachable by client 122 | * [x] Small data size: regular class based styling is added to a style tag 123 | * [x] Vendor prefixes: tooling should exist, but which to use for an isomorphic app wasn’t immediately clear. 124 | * [x] Code+Style locality 125 | * [-] Portability of mobile app code: uses `className` attribute rather than `style` 126 | */ 127 | app.get("/react-aphrodite/artist/:id", (req: any, res: any, next: any) => { 128 | IsomorphicRelay.prepareData({ 129 | Container: ReactAphroditeArtist, 130 | queryConfig: new ArtistQueryConfig({ artistID: req.params.id }), 131 | }, res.locals.networkLayer).then(({ data, props }) => { 132 | const { html, css } = StyleSheetServer.renderStatic(() => { 133 | return ReactDOMServer.renderToString() 134 | }) 135 | res.send(` 136 | 137 | 138 | 139 | 140 | 141 | 146 | 147 | 148 |
${html}
149 | 150 | 151 | `) 152 | }).catch(next) 153 | }) 154 | 155 | /* 156 | * REACT with JSS for inline CSS 157 | * 158 | * [x] Server-side rendering 159 | * [x] Client-side rendering 160 | * [ ] Client-side rehydration: this is the only thing I can find on the topic 161 | * https://github.com/cssinjs/react-jss/issues/2 162 | * [x] No limitation in CSS possibilities: at least I believe media queries and keyframe animations are native? 163 | * [ ] Styling cachable by client 164 | * [x] Small data size: regular class based styling is added to a style tag 165 | * [x] Vendor prefixes: tooling should exist, but which to use for an isomorphic app wasn’t immediately clear. 166 | * [x] Code+Style locality 167 | * [-] Portability of mobile app code: uses `className` attribute rather than `style` 168 | */ 169 | app.get("/react-jss/artist/:id", (req: any, res: any, next: any) => { 170 | IsomorphicRelay.prepareData({ 171 | Container: ReactJSSArtist, 172 | queryConfig: new ArtistQueryConfig({ artistID: req.params.id }), 173 | }, res.locals.networkLayer).then(({ data, props }) => { 174 | const sheets = new SheetsRegistry() 175 | const html = ReactDOMServer.renderToString( 176 | 177 | ) 178 | 179 | ) 180 | res.send(` 181 | 182 | 183 | 184 | 185 | 186 | 190 | 191 | 192 |
${html}
193 | 194 | 195 | `) 196 | }).catch(next) 197 | }) 198 | 199 | /* 200 | * ReactNativeWeb 201 | * 202 | * [x] Server-side rendering 203 | * [x] Client-side rendering 204 | * [ ] Client-side rehydration 205 | * [ ] No limitation in CSS possibilities 206 | * [ ] CSS separately cachable 207 | * [x] Portability of mobile app code 208 | * 209 | * It doesn’t look like ReactNativeWeb supports server-side-rendering + client-side rehydration out-of-the-box. The 210 | * styles generated by the two sides differ from each other. (I suppose it’s agnostic vs vendor specifc CSS prefixes?) 211 | * 212 | * However, this seems like it should be easily fixable. In addition, there’s a PR up that makes it possible to switch 213 | * out the default styling engine with JSS https://github.com/necolas/react-native-web/pull/304. 214 | */ 215 | app.get("/react-native-web/artist/:id", (req: any, res: any, next: any) => { 216 | IsomorphicRelay.prepareData({ 217 | Container: ReactNativeWebArtist, 218 | queryConfig: new ArtistQueryConfig({ artistID: req.params.id }), 219 | }, res.locals.networkLayer).then(({ data, props }) => { 220 | const content = ReactDOMServer.renderToString() 221 | const StyleSheet: any = ReactNative.StyleSheet 222 | const styleElement = StyleSheet.renderToString() 223 | res.send(` 224 | 225 | 226 | 227 | 228 | 231 | ${styleElement} 232 | 233 | 234 |
${content}
235 | 236 | 237 | `) 238 | }).catch(next) 239 | }) 240 | 241 | export default app 242 | -------------------------------------------------------------------------------- /data/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "gray-light": "#f8f8f8", 3 | "gray-regular": "#e5e5e5", 4 | "gray-medium": "#cccccc", 5 | "gray-semibold": "#666666", 6 | "gray-bold": "#333333", 7 | "purple-light": "#e2d2ff", 8 | "purple-regular": "#6e1fff", 9 | "red-regular": "#f7625a", 10 | "yellow-regular": "#fdefd1", 11 | "yellow-medium": "#fce1a8", 12 | "yellow-bold": "#f1af1b", 13 | "green-regular": "#0eda83" 14 | } 15 | -------------------------------------------------------------------------------- /data/schema.js: -------------------------------------------------------------------------------- 1 | var getbabelRelayPlugin = require("babel-relay-plugin") 2 | var schema = require("./schema.json") 3 | 4 | module.exports = getbabelRelayPlugin(schema.data) 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Changes to this file will not be automatically reloaded, 3 | * instead you will have to restart the process to do so. 4 | */ 5 | 6 | // import express from "express"; 7 | const express = require("express"); 8 | const morgan = require("morgan"); 9 | const path = require("path"); 10 | 11 | const app = express(); 12 | 13 | if (process.env.NODE_ENV === "production") { 14 | app.use(morgan("combined")); 15 | app.use(express.static("public")); 16 | } else { 17 | app.use(morgan("dev")); 18 | 19 | // Long symbolicated stack traces 20 | require("longjohn"); 21 | 22 | const webpack = require("webpack"); 23 | const config = require("./webpack.config"); 24 | const compiler = webpack(config); 25 | 26 | // Dynamically host assets to browser. 27 | app.use(require("webpack-dev-middleware")(compiler, { 28 | noInfo: true, 29 | publicPath: config.output.publicPath, 30 | serverSideRender: true, 31 | })); 32 | 33 | // Allow client to be notified of changes to sources. 34 | app.use(require("webpack-hot-middleware")(compiler)); 35 | 36 | // Watch for FS changes in ./app and clear cached modules when a change occurs, 37 | // thus effectively reloading the file on a subsequent request. 38 | const appPath = path.join(__dirname, "app"); 39 | const watcher = require("chokidar").watch(appPath); 40 | watcher.on("ready", function () { 41 | // TODO See if this can be optimsed to reload less files. 42 | // Basically need to know dependency graph of modules, maybe flow can help? 43 | watcher.on("all", function () { 44 | // console.log(`Clearing module cache in: ${appPath}`) 45 | Object.keys(require.cache).forEach(function (id) { 46 | if (id.startsWith(appPath)) { 47 | delete require.cache[id]; 48 | } 49 | }); 50 | }); 51 | }); 52 | 53 | // In case of an uncaught exception show it to the user and proceed, rather than exiting the process. 54 | // NOTE: This is a bad thing when it comes to concurrency, basically you can’t have 2 requests at the same time. 55 | let currentResponse = null; 56 | app.use(function (req, res, next) { 57 | // if (currentResponse) { 58 | // console.error("No concurrent requests may be made, only 1 at a time."); 59 | // process.abort(); 60 | // } 61 | currentResponse = res; 62 | res.on("finish", () => { 63 | currentResponse = null; 64 | }); 65 | next(); 66 | }); 67 | process.on("uncaughtException", (error) => { 68 | if (currentResponse) { 69 | currentResponse.status(500).send(`
${error.stack}
`); 70 | currentResponse = null; 71 | } else { 72 | console.error(error); 73 | process.abort(); 74 | } 75 | }); 76 | } 77 | 78 | // Dynamically load app routes so that they can be reloaded in development. 79 | app.use(function (req, res, next) { 80 | require("./app/routes").default(req, res, next); 81 | }); 82 | 83 | app.listen(3000, () => { 84 | console.log("✨ Listening on http://localhost:3000"); // tslint:disable-line 85 | }); 86 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components", 12 | "jspm_packages", 13 | "tmp", 14 | "temp", 15 | "distribution" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relational-theory", 3 | "version": "0.0.1", 4 | "description": "A prototype of server-side React+Relay rendering.", 5 | "main": "index.js", 6 | "repository": "https://github.com/alloy/relational-theory.git", 7 | "author": "Eloy Durán", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "ts-babel-node index.js", 11 | "test": "yarn run tslint -- --project tsconfig.json app/**/**.{ts,tsx} && jest", 12 | "sync-schema": "cd externals/metaphysics && git fetch && git checkout origin/master && yarn install && npm run dump-schema -- ../../data", 13 | "sync-colors": "cd externals/elan && git fetch && git checkout origin/master && cp components/lib/variables/colors.json ../../data", 14 | "create-types": "gql2ts data/schema.json", 15 | "storybook": "start-storybook -p 6006" 16 | }, 17 | "devDependencies": { 18 | "@kadira/storybook": "^2.21.0", 19 | "@types/isomorphic-fetch": "^0.0.31", 20 | "@types/jest": "^16.0.3", 21 | "@types/node": "^6.0.55", 22 | "@types/react": "^0.14.55", 23 | "@types/react-dom": "^0.14.20", 24 | "@types/react-relay": "^0.9.8", 25 | "babel-cli": "^6.18.0", 26 | "babel-eslint": "^7.1.1", 27 | "babel-plugin-module-resolver": "^2.4.0", 28 | "babel-plugin-syntax-async-functions": "^6.13.0", 29 | "babel-plugin-transform-class-properties": "^6.19.0", 30 | "babel-plugin-transform-flow-strip-types": "^6.21.0", 31 | "babel-plugin-transform-react-jsx": "^6.8.0", 32 | "babel-plugin-transform-regenerator": "^6.21.0", 33 | "babel-polyfill": "^6.20.0", 34 | "babel-preset-es2015": "^6.18.0", 35 | "babel-preset-react": "^6.16.0", 36 | "babel-preset-stage-3": "^6.17.0", 37 | "babel-relay-plugin": "https://github.com/alloy/relay/releases/download/v0.9.3/babel-relay-plugin-0.9.3.tgz", 38 | "chokidar": "^1.6.1", 39 | "jest": "^18.0.0", 40 | "json-loader": "^0.5.4", 41 | "longjohn": "alloy/longjohn#update-source-map-support", 42 | "react-hot-loader": "^1.3.1", 43 | "react-storybooks-relay-container": "^1.0.1", 44 | "storyshots": "orta/storyshots#49bfe52", 45 | "ts-babel-node": "orta/ts-babel-node#tsx", 46 | "ts-jest": "^18.0.1", 47 | "ts-node": "^2.0.0", 48 | "tslint": "^4.3.1", 49 | "webpack": "v2.2.0-rc.5", 50 | "webpack-dev-middleware": "^1.9.0", 51 | "webpack-hot-middleware": "^2.14.0" 52 | }, 53 | "dependencies": { 54 | "aphrodite": "^1.1.0", 55 | "awesome-typescript-loader": "v3.0.0-beta.13", 56 | "express": "^4.14.0", 57 | "isomorphic-fetch": "^2.2.1", 58 | "isomorphic-relay": "^0.7.3", 59 | "morgan": "^1.7.0", 60 | "react": "^15.4.1", 61 | "react-dom": "^15.4.1", 62 | "react-jss": "^5.1.1", 63 | "react-native-web": "^0.0.68", 64 | "react-relay": "https://github.com/alloy/relay/releases/download/v0.9.3/react-relay-0.9.3.tgz", 65 | "typescript": "^2.1.4" 66 | }, 67 | "jest": { 68 | "transform": { 69 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 70 | }, 71 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 72 | "moduleFileExtensions": [ 73 | "ts", 74 | "tsx", 75 | "js" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "sourceMap": true, 6 | "rootDir": ".", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "allowSyntheticDefaultImports": true 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "out", 14 | "dangerfile.js", 15 | "data", 16 | "externals" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "react" 5 | ], 6 | "rules": { 7 | "trailing-comma": [ 8 | false 9 | ], 10 | "arrow-parens": false, 11 | "semicolon": [ 12 | true, 13 | "never", 14 | "ignore-interfaces", 15 | "ignore-bound-class-methods" 16 | ], 17 | "interface-name": [ 18 | true, 19 | "never-prefix" 20 | ], 21 | "member-access": [ 22 | false, 23 | "check-accessor", 24 | "check-constructor" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /types/react-native-web.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "react-native-web" { 4 | export = React 5 | } 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Changes to this file will not be automatically reloaded, 3 | * instead you will have to restart the process to do so. 4 | */ 5 | 6 | var webpack = require("webpack"); 7 | var path = require("path"); 8 | 9 | var { CheckerPlugin } = require("awesome-typescript-loader") 10 | 11 | module.exports = { 12 | entry: { 13 | "pure-react": [ 14 | "./app/containers/pure-react/artist/browser", 15 | "webpack-hot-middleware/client", 16 | ], 17 | "react-aphrodite": [ 18 | "./app/containers/react-aphrodite/artist/browser", 19 | "webpack-hot-middleware/client", 20 | ], 21 | "react-inline-css": [ 22 | "./app/containers/react-inline-css/artist/browser", 23 | "webpack-hot-middleware/client", 24 | ], 25 | "react-jss": [ 26 | "./app/containers/react-jss/artist/browser", 27 | "webpack-hot-middleware/client", 28 | ], 29 | "react-native-web": [ 30 | "./app/containers/react-native-web/artist/browser", 31 | "webpack-hot-middleware/client", 32 | ], 33 | }, 34 | module: { 35 | rules: [ 36 | { test: /\.json$/, loader: "json-loader" }, 37 | { 38 | exclude: /node_modules/, 39 | loaders: ["react-hot-loader", "awesome-typescript-loader?configFileName=./tsconfig.json&silent=true&target=es6&useBabel=true&useCache=true"], 40 | test: /\.tsx?$/, 41 | }, 42 | ], 43 | }, 44 | output: { 45 | filename: "[name].js", 46 | path: path.join(__dirname, "assets"), 47 | publicPath: "/assets", 48 | }, 49 | plugins: [ 50 | new CheckerPlugin(), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.optimize.CommonsChunkPlugin("commons.chunk"), 53 | ], 54 | resolve: { 55 | alias: { 56 | "react-native": "react-native-web/core", 57 | }, 58 | extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], 59 | }, 60 | devtool: "#inline-source-map", // TODO: For production we should output a source-map file instead. 61 | }; 62 | --------------------------------------------------------------------------------