onClose()} />
11 |
{children}
12 | >
13 | )
14 | }
15 |
16 | export default Modal
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .vscode
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | NordicPixels
9 |
10 |
11 | This is a starter application for [framer-motion course](https://octocourses.com/framer-motion/).
12 |
13 | ### Getting started
14 |
15 | To get started, run:
16 |
17 | yarn
18 | yarn start
19 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Route } from 'wouter'
4 | import Home from './components/home'
5 | import Sandbox from './components/sandbox'
6 | import './styles.scss'
7 |
8 | const App = () => (
9 | <>
10 |
11 |
12 | >
13 | )
14 |
15 | ReactDOM.render(
, document.getElementById('root'))
16 |
--------------------------------------------------------------------------------
/src/components/customize.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Carousel from './carousel'
3 |
4 | type Props = {
5 | onComplete: () => void
6 | picture: Picture
7 | }
8 |
9 | const Customize = ({ onComplete, picture }: Props) => {
10 | return (
11 |
12 |
13 |
16 |
17 | )
18 | }
19 |
20 | export default Customize
21 |
--------------------------------------------------------------------------------
/src/components/thumbnail.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | type Props = {
4 | picture: Picture
5 | onClick: (picture: Picture) => void
6 | }
7 |
8 | const Thumbnail = ({ picture, onClick }: Props) => {
9 | return (
10 |
onClick(picture)}>
11 |
17 |
18 | )
19 | }
20 |
21 | export default Thumbnail
22 |
--------------------------------------------------------------------------------
/src/components/grid.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Thumbnail from './thumbnail'
3 |
4 | type Props = {
5 | pictures: readonly Picture[]
6 | onPictureClick: (picture: Picture) => void
7 | }
8 |
9 | const Grid = ({ pictures, onPictureClick }: Props) => {
10 | return (
11 |
12 | {pictures.map(picture => (
13 |
18 | ))}
19 |
20 | )
21 | }
22 |
23 | export default Grid
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
NordicPixels
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import sortBy from 'lodash.sortby'
3 | import Header from './header'
4 | import Grid from './grid'
5 | import Details from './details'
6 | import pictures from '../data'
7 |
8 | const Home = () => {
9 | const [order, setOrder] = useState
('rating')
10 | const [selected, setSelected] = useState(null)
11 |
12 | return (
13 |
14 |
15 |
19 | {selected && (
20 | setSelected(null)}
22 | picture={selected}
23 | />
24 | )}
25 |
26 | )
27 | }
28 |
29 | export default Home
30 |
--------------------------------------------------------------------------------
/src/components/formats.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | type Props = {
4 | src: string
5 | }
6 |
7 | const Square = ({ src }: Props) => (
8 |
16 | )
17 |
18 | const Portrait = ({ src }: Props) => (
19 |
27 | )
28 |
29 | const Landscape = ({ src }: Props) => (
30 |
38 | )
39 |
40 | export default {
41 | Square,
42 | Portrait,
43 | Landscape,
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/complete.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | type Props = {
4 | onComplete: () => void
5 | }
6 |
7 | const Complete = ({ onComplete }: Props) => {
8 | return (
9 |
10 |
29 |
30 |
31 | Thank you for
your order
32 |
33 |
34 | )
35 | }
36 |
37 | export default Complete
38 |
--------------------------------------------------------------------------------
/src/components/review.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | type Props = {
4 | onComplete: () => void
5 | }
6 |
7 | const props: ReadonlyArray<{ name: string; value: string }> = [
8 | { name: 'SKU', value: 'Printed photo' },
9 | { name: 'Delivery type', value: 'expedited' },
10 | { name: 'Price', value: '$5.99' },
11 | ]
12 |
13 | const Review = ({ onComplete }: Props) => {
14 | return (
15 |
16 |
17 |
Review your order
18 |
19 | {props.map(prop => (
20 | -
21 | {prop.name}
22 | {prop.value}
23 |
24 | ))}
25 |
26 |
27 |
30 |
31 | )
32 | }
33 |
34 | export default Review
35 |
--------------------------------------------------------------------------------
/src/components/details.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Modal from './modal'
3 | import Customize from './customize'
4 | import Review from './review'
5 | import Complete from './complete'
6 |
7 | type Props = {
8 | picture: Picture
9 | onClose: () => void
10 | }
11 |
12 | type Step = 'customize' | 'review' | 'complete'
13 |
14 | const Details = ({ onClose, picture }: Props) => {
15 | const [step, setStep] = useState('customize')
16 |
17 | return (
18 |
19 | {step === 'customize' && (
20 | setStep('review')}
23 | />
24 | )}
25 | {step === 'review' && (
26 | setStep('complete')} />
27 | )}
28 | {step === 'complete' && }
29 |
30 | )
31 | }
32 |
33 | export default Details
34 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | type Props = {
4 | order: OrderField
5 | onOrderChange: (newOrder: OrderField) => void
6 | }
7 |
8 | const Header = ({ onOrderChange, order }: Props) => {
9 | return (
10 |
11 |
12 |
13 | NordicPixels
14 |
15 |
16 |
24 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default Header
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nordic-pixels",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "lodash.sortby": "^4.7.0",
14 | "node-sass": "^4.13.0",
15 | "react": "^16.12.0",
16 | "react-dom": "^16.12.0",
17 | "react-scripts": "3.4.0",
18 | "typescript": "~3.7.2",
19 | "wouter": "^2.4.0"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | },
42 | "devDependencies": {
43 | "@types/lodash.sortby": "^4.7.6"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/eye.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/src/data.ts:
--------------------------------------------------------------------------------
1 | /**
2 |
3 | Photos from https://unsplash.com by
4 | Justin Kauffman, Alexander Sinn, Reiseuhu, Carl Cerstrand, Simon Migaj,
5 | Jelle van Leest, Torbjorn Sandbakk, Kym Ellis, Callum Stewart, Kym Ellis,
6 | Mikita Karasiou, John O'Nolan, Yuriy Garnaev, Geran de Klerk
7 |
8 | */
9 |
10 | const pictures: readonly Picture[] = [
11 | { url: '/images/1.jpg', views: 403, rating: 3.7 },
12 | { url: '/images/2.jpg', views: 435, rating: 3.9 },
13 | { url: '/images/3.jpg', views: 408, rating: 4.0 },
14 | { url: '/images/4.jpg', views: 379, rating: 3.2 },
15 | { url: '/images/5.jpg', views: 579, rating: 4.7 },
16 | { url: '/images/6.jpg', views: 429, rating: 4.1 },
17 | { url: '/images/7.jpg', views: 508, rating: 4.5 },
18 | { url: '/images/8.jpg', views: 485, rating: 3.0 },
19 | { url: '/images/9.jpg', views: 351, rating: 4.9 },
20 | { url: '/images/10.jpg', views: 302, rating: 3.7 },
21 | { url: '/images/11.jpg', views: 469, rating: 5.0 },
22 | { url: '/images/12.jpg', views: 367, rating: 4.5 },
23 | { url: '/images/13.jpg', views: 558, rating: 5.0 },
24 | { url: '/images/14.jpg', views: 582, rating: 4.2 },
25 | { url: '/images/15.jpg', views: 449, rating: 3.8 },
26 | { url: '/images/16.jpg', views: 431, rating: 3.8 },
27 | { url: '/images/17.jpg', views: 571, rating: 4.4 },
28 | { url: '/images/18.jpg', views: 514, rating: 4.8 },
29 | ]
30 |
31 | export default pictures
32 |
--------------------------------------------------------------------------------
/public/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/src/components/carousel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Formats from './formats'
3 |
4 | type Props = {
5 | picture: Picture
6 | }
7 |
8 | const wrap = (index: number, total: number) => {
9 | if (index < 0) {
10 | return total - 1
11 | }
12 | if (index === total) {
13 | return 0
14 | }
15 | return index
16 | }
17 |
18 | const options = [
19 | {
20 | label: 'Square (1:1)',
21 | Component: Formats.Square,
22 | },
23 | {
24 | label: 'Portrait (3:4)',
25 | Component: Formats.Portrait,
26 | },
27 | {
28 | label: 'Landscape (7:4)',
29 | Component: Formats.Landscape,
30 | },
31 | ]
32 |
33 | const Carousel = ({ picture }: Props) => {
34 | const [index, setIndex] = useState(0)
35 |
36 | const paginate = (newDirection: number) => {
37 | const newIndex = wrap(index + newDirection, options.length)
38 | setIndex(newIndex)
39 | }
40 |
41 | const Option = options[index]
42 |
43 | return (
44 |
45 |
Choose your format
46 |
47 |
48 |
49 |
50 |
59 |
60 | {options[index].label}
61 |
62 |
63 | )
64 | }
65 |
66 | export default Carousel
67 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @mixin mq-small {
2 | @media (max-width: 52em) {
3 | @content;
4 | }
5 | }
6 |
7 | // GLOBALS
8 |
9 | html {
10 | font-size: 62.5%;
11 | }
12 |
13 | * {
14 | box-sizing: border-box;
15 | }
16 |
17 | body {
18 | margin: 0;
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
20 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
21 | 'Helvetica Neue', sans-serif;
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | background: #f7f8fb;
25 | -webkit-touch-callout: none;
26 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
27 | color: #434343;
28 |
29 | @include mq-small {
30 | user-select: none;
31 | }
32 | }
33 |
34 | // COMPONENTS
35 |
36 | .button {
37 | font-size: 1.6rem;
38 | background: #3b96fa;
39 | border: none;
40 | border-radius: 5rem;
41 | color: #fff;
42 | padding: 2rem 3rem;
43 | box-shadow: 0 0.2rem 0.8rem -0.1rem rgba(39, 94, 254, 0.32);
44 | cursor: pointer;
45 | outline: none;
46 | font-weight: bold;
47 | align-self: end;
48 | margin: 2rem 4rem 4rem 4rem;
49 | width: -webkit-fill-available;
50 |
51 | &:hover {
52 | box-shadow: 0 0.4rem 2rem -0.2rem rgba(39, 94, 254, 0.5);
53 | }
54 | }
55 |
56 | .overlay {
57 | background: rgba(4, 15, 39, 0.8);
58 | width: 100%;
59 | height: 100vh;
60 | position: fixed;
61 | top: 0;
62 | left: 0;
63 | z-index: 3;
64 | backdrop-filter: blur(0.4rem);
65 | }
66 |
67 | .modal-panel {
68 | background: white;
69 | position: fixed;
70 | width: 40rem;
71 | top: 10rem;
72 | left: 0;
73 | right: 0;
74 | margin: 0 auto;
75 | z-index: 4;
76 | min-height: 20rem;
77 | border-radius: 1rem;
78 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2);
79 | user-select: none;
80 | cursor: pointer;
81 |
82 | &:active {
83 | cursor: grab;
84 | }
85 |
86 | @include mq-small {
87 | width: 100%;
88 | height: 80%;
89 | bottom: 0;
90 | top: unset;
91 | position: fixed;
92 | overflow: auto;
93 | border-radius: 1rem 1rem 0 0;
94 | box-shadow: none;
95 | }
96 | }
97 |
98 | .carousel {
99 | background: #e1e1e1;
100 | height: 30rem;
101 | position: relative;
102 | overflow: hidden;
103 |
104 | &-item {
105 | position: absolute;
106 | top: 0;
107 | left: 0;
108 | right: 0;
109 | bottom: 0;
110 | display: flex;
111 | align-items: center;
112 | justify-content: center;
113 | }
114 |
115 | &-label {
116 | background: white;
117 | border-radius: 0.5rem;
118 | padding: 1rem;
119 | box-shadow: 0 0 0.5rem 0 rgba(95, 95, 95, 0.3);
120 | font-size: 1.4rem;
121 | font-weight: bold;
122 | margin: 0 auto;
123 | width: 15rem;
124 | margin-top: -1.5rem;
125 | text-align: center;
126 | color: #4c4c4c;
127 | overflow: hidden;
128 | position: relative;
129 | }
130 |
131 | &-arrow {
132 | position: absolute;
133 | top: 0;
134 | bottom: 0;
135 | width: 3rem;
136 | background-color: transparent;
137 | background-repeat: no-repeat;
138 | background-position: center;
139 | border: none;
140 | outline: none;
141 | opacity: 0.5;
142 |
143 | &--left {
144 | left: 0;
145 | background-image: url(/chevron-left.png);
146 | }
147 |
148 | &--right {
149 | right: 0;
150 | background-image: url(/chevron-right.png);
151 | }
152 | }
153 | }
154 |
155 | // SCREENS
156 |
157 | .home-screen {
158 | .header {
159 | background: white;
160 | box-shadow: 0 0 0.8rem 1rem rgba(234, 234, 234, 0.35);
161 | position: sticky;
162 | top: 0;
163 | z-index: 1;
164 |
165 | &-inner {
166 | height: 11rem;
167 | max-width: 60rem;
168 | margin: 0 auto;
169 |
170 | .logo {
171 | text-align: center;
172 | padding: 2rem;
173 | font-weight: bold;
174 | font-size: 2rem;
175 |
176 | & > span {
177 | color: #3b96fa;
178 | }
179 | }
180 | }
181 |
182 | .sort-buttons {
183 | display: flex;
184 |
185 | .sort-button {
186 | font-size: 1.6rem;
187 | flex: 1;
188 | display: flex;
189 | align-items: center;
190 | justify-content: center;
191 | background: none;
192 | height: 4rem;
193 | width: 4rem;
194 | outline: none;
195 | border: none;
196 | color: #434343;
197 | font-weight: bold;
198 | opacity: 0.3;
199 |
200 | & > img {
201 | margin-right: 1rem;
202 | width: 1.6rem;
203 | }
204 | }
205 |
206 | .active {
207 | opacity: 1;
208 | }
209 | }
210 | }
211 |
212 | .thumbnails {
213 | display: grid;
214 | grid-template-columns: repeat(3, 1fr);
215 | max-width: 60rem;
216 | margin: 0 auto;
217 | row-gap: 0.5rem;
218 | column-gap: 0.5rem;
219 | padding: 0.5rem;
220 |
221 | .thumbnail {
222 | width: 100%;
223 | padding-top: 100%;
224 | position: relative;
225 | cursor: pointer;
226 |
227 | &-image {
228 | background-size: cover;
229 | position: absolute;
230 | top: 0;
231 | left: 0;
232 | right: 0;
233 | bottom: 0;
234 | border-radius: 0.5rem;
235 | }
236 | }
237 | }
238 | }
239 |
240 | .customize-screen {
241 | display: grid;
242 | grid-template-rows: 4fr 1fr;
243 | height: 100%;
244 |
245 | h3 {
246 | margin: 2.5rem auto;
247 | text-align: center;
248 | text-transform: uppercase;
249 | font-size: 1.4rem;
250 | }
251 | }
252 |
253 | .review-screen {
254 | display: flex;
255 | flex-direction: column;
256 | align-items: center;
257 | justify-content: space-between;
258 | height: 100%;
259 |
260 | h2 {
261 | margin-top: 7rem;
262 | font-size: 2.4rem;
263 | margin-bottom: 3rem;
264 | }
265 |
266 | ul {
267 | list-style: none;
268 | padding: 0;
269 | font-size: 1.6rem;
270 |
271 | li {
272 | display: flex;
273 | justify-content: space-between;
274 | margin-bottom: 1rem;
275 | font-weight: bold;
276 |
277 | span:first-child {
278 | color: #a8a8a8;
279 | font-weight: normal;
280 | }
281 | }
282 | }
283 | }
284 |
285 | .complete-screen {
286 | text-align: center;
287 | padding: 15rem 0;
288 |
289 | svg {
290 | width: 10rem;
291 | }
292 |
293 | h1 {
294 | color: #4f4f4f;
295 | font-size: 2.4rem;
296 | }
297 | }
298 |
299 | // SANDBOX
300 |
301 | .sandbox-screen {
302 | padding: 2rem;
303 |
304 | .box {
305 | width: 10rem;
306 | height: 10rem;
307 | background: #09f;
308 | margin-bottom: 1rem;
309 | }
310 | }
311 |
--------------------------------------------------------------------------------