├── src
├── components
│ ├── Card.js
│ ├── Header.js
│ ├── Home.js
│ ├── ScrollToTopOnMount.js
│ ├── HouseList.js
│ ├── HouseImage.js
│ ├── NotFound.js
│ ├── Map.js
│ ├── Grid.js
│ ├── RequestViewingForm.js
│ ├── HouseCard.js
│ ├── HouseSummary.js
│ ├── HouseCardWithAnimation.js
│ ├── AnimateTransitionToHouse.js
│ └── House.js
├── index.css
├── index.js
├── App.js
└── data
│ └── houses.js
├── public
├── favicon.ico
├── images
│ ├── house1.jpg
│ ├── house2.jpg
│ ├── house3.jpg
│ ├── house4.jpg
│ ├── house5.jpg
│ └── house6.jpg
├── manifest.json
└── index.html
├── README.md
├── .gitignore
└── package.json
/src/components/Card.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
--------------------------------------------------------------------------------
/public/images/house1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/images/house1.jpg
--------------------------------------------------------------------------------
/public/images/house2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/images/house2.jpg
--------------------------------------------------------------------------------
/public/images/house3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/images/house3.jpg
--------------------------------------------------------------------------------
/public/images/house4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/images/house4.jpg
--------------------------------------------------------------------------------
/public/images/house5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/images/house5.jpg
--------------------------------------------------------------------------------
/public/images/house6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjparsons/housetopia/HEAD/public/images/house6.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Housetopia
2 |
3 | This is a project demonstrating using React Router v4 and implementing some different animations.
4 |
5 | * [Live app](http://housetopia.sjparsons.com)
6 | * [Slides](http://router-4-animation.sjparsons.com)
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import {BrowserRouter as Router} from 'react-router-dom';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Link} from 'react-router-dom'
3 |
4 | const style = {
5 | cursor: 'pointer'
6 | }
7 |
8 | const Header = ({history}) => (
9 |
12 | );
13 |
14 | export default Header;
15 |
--------------------------------------------------------------------------------
/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import HouseList from './HouseList'
3 | import { houses } from '../data/houses'
4 |
5 | const Home = () => (
6 |
7 |
Welcome, you might be interested in the following houses.
8 |
9 |
10 | );
11 |
12 | export default Home;
--------------------------------------------------------------------------------
/src/components/ScrollToTopOnMount.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * Straight from the React Router v4 docs
5 | */
6 | export default class ScrollToTopOnMount extends React.Component {
7 | componentDidMount(prevProps) {
8 | window.scrollTo(0, 0)
9 | }
10 |
11 | render() {
12 | return null
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/HouseList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HouseCardWithAnimation from './HouseCardWithAnimation';
3 | import { Grid } from './Grid';
4 |
5 | const HouseList = ({houses}) => (
6 |
7 | { houses.map( house =>
8 |
9 | )}
10 |
11 | );
12 |
13 | export default HouseList;
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/src/components/HouseImage.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const HouseImage = styled.div`
4 | background-image: url(${ props => props.image });
5 | background-size: cover;
6 | background-position: center center;
7 | width: 100%;
8 | height: ${props => props.height};
9 | transition: height 200ms;
10 | position: relative;
11 | `;
12 |
13 | export default HouseImage;
--------------------------------------------------------------------------------
/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const CenterContent = styled.div`
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | font-size: 2em;
9 | `;
10 |
11 | const NotFound = ({location}) => {
12 | return (
13 | 404 Aw, shucks! {location.pathname} cannot be found
14 | );
15 | };
16 |
17 | export default NotFound;
--------------------------------------------------------------------------------
/src/components/Map.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const getMapUrl = ({mode, apiKey, params}) =>
4 | `https://www.google.com/maps/embed/v1/${mode}?key=${apiKey}&${params}`;
5 |
6 | const Map = ({width, height, ...mapProps}) => (
7 |
16 | );
17 |
18 | export default Map;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "router-4-animations",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^15.6.1",
7 | "react-dom": "^15.6.1",
8 | "react-router-dom": "^4.1.2",
9 | "react-scripts": "1.0.10",
10 | "styled-components": "^2.1.1"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "test": "react-scripts test --env=jsdom",
16 | "eject": "react-scripts eject",
17 | "deploy": "surge build housetopia.sjparsons.com"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Grid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | /**
4 | * A poor man's mobile-first responsive grid
5 | */
6 |
7 | export const Grid = styled.div`
8 | display: block;
9 | width: 100%;
10 |
11 | @media (min-width: 601px) {
12 | display: flex;
13 | flex-wrap: wrap;
14 |
15 | > * {
16 | flex: 0 0 50%;
17 | }
18 | }
19 |
20 | @media (min-width: 850px) {
21 | > * {
22 | flex: 0 0 33%;
23 | }
24 | }
25 |
26 | @media (min-width: 1000px) {
27 | > * {
28 | flex: 0 0 25%;
29 | }
30 | }
31 |
32 | `;
33 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route, Switch} from 'react-router-dom';
3 |
4 | import Home from './components/Home'
5 | import House from './components/House'
6 | import Header from './components/Header'
7 | import NotFound from './components/NotFound'
8 |
9 | const App = () => (
10 |
11 |
12 |
13 |
14 | } />
15 |
16 |
17 |
18 | );
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/components/RequestViewingForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // TODO add a nice form library ()
4 |
5 | const RequestViewingForm = ({house}) => {
6 | return (
7 |
21 | )
22 | };
23 |
24 | export default RequestViewingForm;
--------------------------------------------------------------------------------
/src/components/HouseCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import {Link} from 'react-router-dom';
4 | import HouseImage from './HouseImage';
5 |
6 | const HouseCard = ({id, image, address: {street, city, state, zip}, ...otherProps}) => (
7 |
8 |
9 | {street}
10 | {city}, {state} {zip}
11 |
12 | );
13 |
14 | export default styled(HouseCard)`
15 | display: block;
16 | color: black;
17 | text-decoration: none;
18 | padding: 10px;
19 | box-sizing: border-box;
20 | transition: background-color 800ms;
21 | &:hover {
22 | background-color: #f0f0f0;
23 | transition: background-color 200ms;
24 | }
25 | `;
--------------------------------------------------------------------------------
/src/components/HouseSummary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import {Link} from 'react-router-dom';
4 |
5 | const HouseSummary = ({id, ...otherProps}) => (
6 |
7 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
8 |
9 |
10 | Request a viewing
11 |
12 |
13 |
14 | );
15 |
16 | export default styled(HouseSummary)`
17 | box-sizing: border-box;
18 | padding: 15px 25px;
19 | `;
--------------------------------------------------------------------------------
/src/data/houses.js:
--------------------------------------------------------------------------------
1 | export const houses = [
2 | {
3 | id: '4893',
4 | address: {
5 | street: "23 Circadian Circle",
6 | city: "White River Junction",
7 | state: "NH",
8 | zip: "43432"
9 | },
10 | image: '/images/house1.jpg'
11 | },
12 | {
13 | id: '5324',
14 | address: {
15 | street: "5023 Aldrich Ave N",
16 | city: "Minneapolis",
17 | state: "MN",
18 | zip: "55432"
19 | },
20 | image: '/images/house2.jpg'
21 | },
22 | {
23 | id: '7438',
24 | address: {
25 | street: "14 Waldorf Dr",
26 | city: "Omaha",
27 | state: "NE",
28 | zip: "12232"
29 | },
30 | image: '/images/house3.jpg'
31 | },
32 | {
33 | id: '8932',
34 | address: {
35 | street: "615 Billings Ave",
36 | city: "Brownsville",
37 | state: "FL",
38 | zip: "23678"
39 | },
40 | image: '/images/house4.jpg'
41 | },
42 | {
43 | id: '5632',
44 | address: {
45 | street: "53281 US 43",
46 | city: "Colorado Springs",
47 | state: "CO",
48 | zip: "87932"
49 | },
50 | image: '/images/house5.jpg'
51 | },
52 | {
53 | id: '7894',
54 | address: {
55 | street: "9002 Hepburn Gardens",
56 | city: "Macon",
57 | state: "GA",
58 | zip: "77889"
59 | },
60 | image: '/images/house6.jpg'
61 | }
62 | ];
63 |
64 |
--------------------------------------------------------------------------------
/src/components/HouseCardWithAnimation.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import HouseImage from './HouseImage';
4 | import AnimateTransitionToHouse from './AnimateTransitionToHouse';
5 |
6 | const Card = styled.div`
7 | display: block;
8 | color: black;
9 | text-decoration: none;
10 | padding: 10px;
11 | box-sizing: border-box;
12 | transition: background-color 800ms;
13 | cursor: pointer;
14 | &:hover {
15 | background-color: #f0f0f0;
16 | transition: background-color 200ms;
17 | }
18 | `;
19 |
20 | class HouseCardWithAnimation extends React.Component {
21 | constructor(props) {
22 | super(props);
23 | this.state = { goToHouse: false, dimensions: {} };
24 | this.handleClick = this.handleClick.bind(this);
25 | }
26 |
27 | handleClick() {
28 | const { top, left, width, height } = this.image._reactInternalInstance.getHostNode().getBoundingClientRect();
29 | this.setState({ goToHouse: true, dimensions: { top, left, width, height} })
30 | }
31 |
32 | render() {
33 | const {id, image, address: {street, city, state, zip}} = this.props;
34 | return (
35 |
36 | { this.state.goToHouse &&
37 |
38 | }
39 | this.image = ref }
43 | />
44 | {street}
45 | {city}, {state} {zip}
46 |
47 | );
48 |
49 | }
50 | };
51 |
52 |
53 | export default HouseCardWithAnimation;
--------------------------------------------------------------------------------
/src/components/AnimateTransitionToHouse.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import styled from 'styled-components';
3 | import { Redirect } from 'react-router-dom';
4 |
5 | const ANIMATION_DURATION = 180;
6 |
7 | const DESTINATION = `
8 | top: 80px;
9 | left: 0;
10 | width: 100%;
11 | height: 300px;
12 | `;
13 |
14 | const Animation = styled.div`
15 | background-image: url(${props => props.image});
16 | background-size: cover;
17 | background-position: center center;
18 | border-bottom: 1000px solid white;
19 | position: fixed;
20 | z-index: 100;
21 | transition: top 150ms, left 150ms, width 150ms, height 150ms;
22 | ${ ({dimensions, inProgress}) => inProgress ? DESTINATION : `
23 | top: ${dimensions.top}px;
24 | left: ${dimensions.left}px;
25 | width: ${dimensions.width}px;
26 | height: ${dimensions.height}px;
27 | `}
28 | `;
29 |
30 | class AnimateTransitionToHouse extends Component {
31 | constructor(props) {
32 | super(props);
33 | this.state = {
34 | inProgress: false,
35 | complete: false
36 | }
37 | }
38 |
39 | componentDidMount() {
40 | // 1. Start the animation
41 | setTimeout(() => {
42 | this.setState({inProgress: true});
43 | }, 10);
44 |
45 | // 2. Mark animation as complete; declaratively render redirect.
46 | setTimeout(() => {
47 | this.setState({complete: true})
48 | }, ANIMATION_DURATION);
49 | }
50 |
51 | render() {
52 | if (this.state.complete)
53 | return ;
54 | else
55 | return ;
56 | }
57 | }
58 |
59 | export default AnimateTransitionToHouse;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Housetopia
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/House.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Route } from 'react-router-dom';
4 | import {houses} from '../data/houses';
5 | import HouseImage from './HouseImage';
6 | import Map from './Map';
7 | import HouseSummary from './HouseSummary';
8 | import RequestViewingForm from './RequestViewingForm';
9 | import ScrollToTopOnMount from './ScrollToTopOnMount';
10 |
11 | const MAP_API_KEY = 'AIzaSyDoEKTDIXDrskeTDNXGSGwEbGQVkDtFxb4';
12 |
13 | const HouseDetailsViewer = styled.div`
14 | width: 100%;
15 | overflow: hidden;
16 | `;
17 |
18 | const HouseDetails = styled.div`
19 | display: flex;
20 | position: relative;
21 | left: ${ props => props.leftOffset };
22 | transition: left 200ms;
23 | > * {
24 | flex: 0 0 50%;
25 | }
26 | `;
27 |
28 | const HouseTitle = styled.div`
29 | position: absolute;
30 | bottom: 0;
31 | width: 100%;
32 | padding: 10px;
33 | color: white;
34 | background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,0.95) 100%);
35 | font-size: 1.5em;
36 | font-weight: bold;
37 | letter-spacing: 1px;
38 | padding-top: 40px;
39 | `;
40 |
41 | const House = ({id, match}) => {
42 | const house = houses.find( house => house.id === id);
43 | const {address: {street, city, state, zip}, image, summary} = house;
44 |
45 | return (
46 |
47 |
48 |
49 | {/*
50 | Always render the contents. Use value of `match` param to alter
51 | contents if needed.
52 | */}
53 |
(
54 |
55 |
56 | {street}, {city}, {state} {zip}
57 |
58 |
59 |
60 |
61 |
62 | {/* Panel 1, 50% width */}
63 |
69 |
70 | {/* Panel 2, 50% width */}
71 |
72 |
73 | {/* Panel 3, 50% width */}
74 |
75 |
76 |
77 |
78 |
79 |
80 | )}/>
81 |
82 |
83 | );
84 | };
85 |
86 | export default House;
--------------------------------------------------------------------------------