├── .gitignore
├── src
├── app
│ ├── components
│ │ ├── Loader
│ │ │ ├── index.jsx
│ │ │ └── Loader.css
│ │ ├── ImageGrid
│ │ │ ├── index.jsx
│ │ │ └── ImageGrid.css
│ │ ├── Input
│ │ │ ├── Input.css
│ │ │ └── index.jsx
│ │ ├── Button
│ │ │ ├── index.jsx
│ │ │ └── Button.css
│ │ ├── Container
│ │ │ ├── index.jsx
│ │ │ └── Container.css
│ │ ├── Swatch
│ │ │ ├── index.jsx
│ │ │ └── Swatch.css
│ │ └── Image
│ │ │ ├── Image.css
│ │ │ └── index.jsx
│ └── App.jsx
├── index.js
└── index.html
├── dist
├── index.html
├── index.2dc47d80.css
└── index.2dc47d80.css.map
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .parcel-cache
--------------------------------------------------------------------------------
/src/app/components/Loader/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './Loader.css'
4 |
5 | export const Loader = () => {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/components/ImageGrid/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './ImageGrid.css'
4 |
5 | export const ImageGrid = ({ children }) => {
6 | return {children}
7 | }
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 |
4 | import 'modern-css-reset'
5 |
6 | import { App } from './app/App'
7 |
8 | render(, document.querySelector('#app'))
9 |
--------------------------------------------------------------------------------
/src/app/components/Input/Input.css:
--------------------------------------------------------------------------------
1 | .input {
2 | border: 4px solid black;
3 | background: white;
4 |
5 | font-family: monospace;
6 | font-size: 16px;
7 |
8 | padding: 10px;
9 | margin-right: 30px;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/components/Button/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './Button.css'
4 |
5 | export const Button = ({ children, onClick }) => {
6 | return (
7 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/components/ImageGrid/ImageGrid.css:
--------------------------------------------------------------------------------
1 | .grid {
2 | position: relative;
3 | display: grid;
4 |
5 | grid-template-columns: 1fr 1fr 1fr;
6 | column-gap: 30px;
7 | row-gap: 30px;
8 |
9 | width: 630px;
10 | }
11 |
12 | .grid > * {
13 | place-self: center;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/components/Button/Button.css:
--------------------------------------------------------------------------------
1 | .button {
2 | border: 4px solid black;
3 | background: white;
4 |
5 | font-family: monospace;
6 | font-size: 16px;
7 |
8 | padding: 10px;
9 | margin-right: 10px;
10 |
11 | outline: none;
12 | }
13 |
14 | .button:active {
15 | background: #ccc;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/components/Container/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './Container.css'
4 |
5 | export const Container = ({ children }) => {
6 | return {children}
7 | }
8 |
9 | export const Header = ({ children }) => {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/components/Input/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './Input.css'
4 |
5 | export const Input = ({ value, onChange, placeholder }) => {
6 | return (
7 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sort Photos by Color
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 | Sort Photos by Color
--------------------------------------------------------------------------------
/src/app/components/Swatch/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './Swatch.css'
4 |
5 | export const Swatch = ({ children, color, onClick, active, value }) => {
6 | return (
7 |
8 |
{children}
9 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/components/Container/Container.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 |
4 | grid-template-columns: auto [content-start] 960px [content-end] auto;
5 | grid-template-rows: 50px 100px auto 100px;
6 | grid-template-areas: '. . .' 'header header header' '. content .' 'footer footer footer';
7 | }
8 |
9 | .container > * {
10 | grid-area: content;
11 | justify-self: center;
12 | }
13 |
14 | .header {
15 | font-family: monospace;
16 | grid-row: 2;
17 | grid-column: 2;
18 |
19 | width: 100%;
20 | display: flex;
21 |
22 | align-items: center;
23 | justify-content: space-between;
24 | position: fixed;
25 | z-index: 100;
26 | padding: 30px;
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/components/Image/Image.css:
--------------------------------------------------------------------------------
1 | .imageContainer {
2 | position: relative;
3 |
4 | width: 200px;
5 | height: 170px;
6 | }
7 |
8 | .image {
9 | width: 170px;
10 | height: 170px;
11 |
12 | border: 4px solid black;
13 |
14 | object-fit: cover;
15 | }
16 |
17 | .colorBoxContainer {
18 | position: absolute;
19 | right: 4px;
20 | bottom: 0;
21 | height: 100%;
22 | width: 30px;
23 |
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: flex-end;
27 | }
28 |
29 | .colorBox {
30 | margin-left: 4px;
31 | width: 19px;
32 | height: 20px;
33 |
34 | border: 4px solid black;
35 | border-left: 0;
36 | }
37 |
38 | .activeBox {
39 | width: 30px;
40 | margin-left: 0;
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/components/Loader/Loader.css:
--------------------------------------------------------------------------------
1 | .loader {
2 | display: inline-block;
3 | position: relative;
4 | width: 80px;
5 | height: 80px;
6 | }
7 | .loader:after {
8 | content: ' ';
9 | display: block;
10 | border-radius: 50%;
11 | width: 0;
12 | height: 0;
13 | margin: 8px;
14 | box-sizing: border-box;
15 | border: 32px solid #000;
16 | border-color: #000 transparent #000 transparent;
17 | animation: lds-hourglass 1.2s infinite;
18 | }
19 | @keyframes lds-hourglass {
20 | 0% {
21 | transform: rotate(0);
22 | animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
23 | }
24 | 50% {
25 | transform: rotate(900deg);
26 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
27 | }
28 | 100% {
29 | transform: rotate(1800deg);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sort-photos-by-color",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "dist/index.html",
6 | "scripts": {
7 | "start": "parcel src/index.html",
8 | "build": "parcel build --public-url /sort-photos-by-color/dist/ src/index.html"
9 | },
10 | "author": "Artur Wojciechowski",
11 | "license": "UNLICENSED",
12 | "devDependencies": {
13 | "parcel": "^2.0.0-beta.1"
14 | },
15 | "dependencies": {
16 | "chroma-js": "^2.1.0",
17 | "modern-css-reset": "^1.3.0",
18 | "node-vibrant": "^3.2.1-alpha.1",
19 | "react": "^17.0.1",
20 | "react-dom": "^17.0.1",
21 | "react-flip-move": "^3.0.4"
22 | },
23 | "browserslist": [
24 | "last 2 Chrome versions"
25 | ],
26 | "prettier": {
27 | "semi": false,
28 | "singleQuote": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/components/Image/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react'
2 |
3 | import './Image.css'
4 |
5 | const toStringColor = ([r, g, b]) => `rgb(${r}, ${g}, ${b})`
6 |
7 | export const Image = forwardRef(
8 | ({ src, palette, activeColor, setActive }, ref) => {
9 | return (
10 |
11 |

12 |
13 | {Object.values(palette).map((color, i) => (
14 |
setActive(i)}
20 | >
21 | ))}
22 |
23 |
24 | )
25 | }
26 | )
27 |
--------------------------------------------------------------------------------
/src/app/components/Swatch/Swatch.css:
--------------------------------------------------------------------------------
1 | .swatch-container {
2 | display: flex;
3 |
4 | align-items: center;
5 | justify-content: flex-end;
6 |
7 | padding: 10px;
8 | }
9 |
10 | .swatch {
11 | border: 4px solid black;
12 | background: white;
13 |
14 | font-family: monospace;
15 | font-size: 32px;
16 | font-style: bold;
17 | line-height: 0;
18 |
19 | padding: 10px;
20 | margin-right: 10px;
21 |
22 | min-width: 50px;
23 | min-height: 50px;
24 | outline: none;
25 | }
26 |
27 | .swatch:active {
28 | border-bottom-width: 2px;
29 | border-top-width: 6px;
30 | }
31 |
32 | .swatch.active {
33 | outline: solid 5px rgba(255, 0, 0, 1);
34 | outline-offset: 2px;
35 | animation-name: my-anim;
36 | animation-duration: 2s;
37 | animation-iteration-count: infinite;
38 | }
39 |
40 | @keyframes my-anim {
41 | 0% {
42 | outline-color: rgba(255, 0, 0, 1);
43 | }
44 |
45 | 50% {
46 | outline-color: rgba(255, 0, 0, 0);
47 | }
48 |
49 | 100% {
50 | outline-color: rgba(255, 0, 0, 1);
51 | }
52 | }
53 |
54 | .swatch-label {
55 | background: white;
56 |
57 | font-family: monospace;
58 | font-size: 16px;
59 |
60 | margin-right: 10px;
61 | }
62 |
--------------------------------------------------------------------------------
/dist/index.2dc47d80.css:
--------------------------------------------------------------------------------
1 | *,:after,:before{box-sizing:border-box}blockquote,body,dd,dl,figure,h1,h2,h3,h4,p{margin:0}ol[role=list],ul[role=list]{list-style:none}html{scroll-behavior:smooth}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}button,input,select,textarea{font:inherit}@media(prefers-reduced-motion:reduce){*,:after,:before{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}.container{display:grid;grid-template-columns:auto [content-start] 960px [content-end] auto;grid-template-rows:50px 100px auto 100px;grid-template-areas:". . ." "header header header" ". content ." "footer footer footer"}.container>*{grid-area:content;justify-self:center}.header{font-family:monospace;grid-row:2;grid-column:2;width:100%;display:flex;align-items:center;justify-content:space-between;position:fixed;z-index:100;padding:30px}.imageContainer{position:relative;width:200px;height:170px}.image{width:170px;height:170px;border:4px solid #000;object-fit:cover}.colorBoxContainer{position:absolute;right:4px;bottom:0;height:100%;width:30px;display:flex;flex-direction:column;justify-content:flex-end}.colorBox{margin-left:4px;width:19px;height:20px;border:4px solid #000;border-left:0}.activeBox{width:30px;margin-left:0}.grid{position:relative;display:grid;grid-template-columns:1fr 1fr 1fr;column-gap:30px;row-gap:30px;width:630px}.grid>*{place-self:center}.button{border:4px solid #000;background:#fff;font-family:monospace;font-size:16px;padding:10px;margin-right:10px;outline:none}.button:active{background:#ccc}.swatch-container{display:flex;align-items:center;justify-content:flex-end;padding:10px}.swatch{border:4px solid #000;background:#fff;font-family:monospace;font-size:32px;font-style:bold;line-height:0;padding:10px;margin-right:10px;min-width:50px;min-height:50px;outline:none}.swatch:active{border-bottom-width:2px;border-top-width:6px}.swatch.active{outline:5px solid red;outline-offset:2px;animation-name:my-anim;animation-duration:2s;animation-iteration-count:infinite}@keyframes my-anim{0%{outline-color:red}50%{outline-color:rgba(255,0,0,0)}to{outline-color:red}}.swatch-label{background:#fff;font-family:monospace;font-size:16px;margin-right:10px}.loader{display:inline-block;position:relative;width:80px;height:80px}.loader:after{content:" ";display:block;border-radius:50%;width:0;height:0;margin:8px;box-sizing:border-box;border-color:#000 transparent;border-style:solid;border-width:32px;animation:lds-hourglass 1.2s infinite}@keyframes lds-hourglass{0%{transform:rotate(0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}50%{transform:rotate(900deg);animation-timing-function:cubic-bezier(.215,.61,.355,1)}to{transform:rotate(5turn)}}.input{border:4px solid #000;background:#fff;font-family:monospace;font-size:16px;padding:10px;margin-right:30px}
2 | /*# sourceMappingURL=index.2dc47d80.css.map */
3 |
--------------------------------------------------------------------------------
/dist/index.2dc47d80.css.map:
--------------------------------------------------------------------------------
1 | {"mappings":"AAAA,iBAAA,qBAAA,CAAA,2CAAA,QAAA,CAAA,4BAAA,eAAA,CAAA,KAAA,sBAAA,CAAA,KAAA,gBAAA,CAAA,4BAAA,CAAA,eAAA,CAAA,eAAA,6BAAA,CAAA,YAAA,cAAA,CAAA,aAAA,CAAA,6BAAA,YAAA,CAAA,sCAAA,iBAAA,kCAAA,CAAA,qCAAA,CAAA,mCAAA,CAAA,8BAAA,CAAA,CCAA,WACA,YAAA,CAEA,mEAAA,CACA,wCAAA,CACA,uFACA,CAEA,aACA,iBAAA,CACA,mBACA,CAEA,QACA,qBAAA,CACA,UAAA,CACA,aAAA,CAEA,UAAA,CACA,YAAA,CAEA,kBAAA,CACA,6BAAA,CACA,cAAA,CACA,WAAA,CACA,YACA,CC1BA,gBACA,iBAAA,CAEA,WAAA,CACA,YACA,CAEA,OACA,WAAA,CACA,YAAA,CAEA,qBAAA,CAEA,gBACA,CAEA,mBACA,iBAAA,CACA,SAAA,CACA,QAAA,CACA,WAAA,CACA,UAAA,CAEA,YAAA,CACA,qBAAA,CACA,wBACA,CAEA,UACA,eAAA,CACA,UAAA,CACA,WAAA,CAEA,qBAAA,CACA,aACA,CAEA,WACA,UAAA,CACA,aACA,CCxCA,MACA,iBAAA,CACA,YAAA,CAEA,iCAAA,CACA,eAAA,CACA,YAAA,CAEA,WACA,CAEA,QACA,iBACA,CCbA,QACA,qBAAA,CACA,eAAA,CAEA,qBAAA,CACA,cAAA,CAEA,YAAA,CACA,iBAAA,CAEA,YACA,CAEA,eACA,eACA,CCfA,kBACA,YAAA,CAEA,kBAAA,CACA,wBAAA,CAEA,YACA,CAEA,QACA,qBAAA,CACA,eAAA,CAEA,qBAAA,CACA,cAAA,CACA,eAAA,CACA,aAAA,CAEA,YAAA,CACA,iBAAA,CAEA,cAAA,CACA,eAAA,CACA,YACA,CAEA,eACA,uBAAA,CACA,oBACA,CAEA,eACA,qBAAA,CACA,kBAAA,CACA,sBAAA,CACA,qBAAA,CACA,kCACA,CAEA,mBACA,GACA,iBACA,CAEA,IACA,6BACA,CAEA,GACA,iBACA,CACA,CAEA,cACA,eAAA,CAEA,qBAAA,CACA,cAAA,CAEA,iBACA,CC5DA,QACA,oBAAA,CACA,iBAAA,CACA,UAAA,CACA,WACA,CACA,cACA,WAAA,CACA,aAAA,CACA,iBAAA,CACA,OAAA,CACA,QAAA,CACA,UAAA,CACA,qBAAA,CAEA,6BAAA,CAAA,kBAAA,CAAA,iBAAA,CACA,qCACA,CACA,yBACA,GACA,mBAAA,CACA,yDACA,CACA,IACA,wBAAA,CACA,uDACA,CACA,GACA,uBACA,CACA,CC9BA,OACA,qBAAA,CACA,eAAA,CAEA,qBAAA,CACA,cAAA,CAEA,YAAA,CACA,iBACA","sources":["./node_modules/modern-css-reset/dist/reset.min.css","./src/app/components/Container/Container.css","./src/app/components/Image/Image.css","./src/app/components/ImageGrid/ImageGrid.css","./src/app/components/Button/Button.css","./src/app/components/Swatch/Swatch.css","./src/app/components/Loader/Loader.css","./src/app/components/Input/Input.css"],"sourcesContent":["*,*::before,*::after{box-sizing:border-box}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin:0}ul[role=\"list\"],ol[role=\"list\"]{list-style:none}html{scroll-behavior:smooth}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}\n",".container {\n display: grid;\n\n grid-template-columns: auto [content-start] 960px [content-end] auto;\n grid-template-rows: 50px 100px auto 100px;\n grid-template-areas: '. . .' 'header header header' '. content .' 'footer footer footer';\n}\n\n.container > * {\n grid-area: content;\n justify-self: center;\n}\n\n.header {\n font-family: monospace;\n grid-row: 2;\n grid-column: 2;\n\n width: 100%;\n display: flex;\n\n align-items: center;\n justify-content: space-between;\n position: fixed;\n z-index: 100;\n padding: 30px;\n}\n",".imageContainer {\n position: relative;\n\n width: 200px;\n height: 170px;\n}\n\n.image {\n width: 170px;\n height: 170px;\n\n border: 4px solid black;\n\n object-fit: cover;\n}\n\n.colorBoxContainer {\n position: absolute;\n right: 4px;\n bottom: 0;\n height: 100%;\n width: 30px;\n\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n}\n\n.colorBox {\n margin-left: 4px;\n width: 19px;\n height: 20px;\n\n border: 4px solid black;\n border-left: 0;\n}\n\n.activeBox {\n width: 30px;\n margin-left: 0;\n}\n",".grid {\n position: relative;\n display: grid;\n\n grid-template-columns: 1fr 1fr 1fr;\n column-gap: 30px;\n row-gap: 30px;\n\n width: 630px;\n}\n\n.grid > * {\n place-self: center;\n}\n",".button {\n border: 4px solid black;\n background: white;\n\n font-family: monospace;\n font-size: 16px;\n\n padding: 10px;\n margin-right: 10px;\n\n outline: none;\n}\n\n.button:active {\n background: #ccc;\n}\n",".swatch-container {\n display: flex;\n\n align-items: center;\n justify-content: flex-end;\n\n padding: 10px;\n}\n\n.swatch {\n border: 4px solid black;\n background: white;\n\n font-family: monospace;\n font-size: 32px;\n font-style: bold;\n line-height: 0;\n\n padding: 10px;\n margin-right: 10px;\n\n min-width: 50px;\n min-height: 50px;\n outline: none;\n}\n\n.swatch:active {\n border-bottom-width: 2px;\n border-top-width: 6px;\n}\n\n.swatch.active {\n outline: solid 5px rgba(255, 0, 0, 1);\n outline-offset: 2px;\n animation-name: my-anim;\n animation-duration: 2s;\n animation-iteration-count: infinite;\n}\n\n@keyframes my-anim {\n 0% {\n outline-color: rgba(255, 0, 0, 1);\n }\n\n 50% {\n outline-color: rgba(255, 0, 0, 0);\n }\n\n 100% {\n outline-color: rgba(255, 0, 0, 1);\n }\n}\n\n.swatch-label {\n background: white;\n\n font-family: monospace;\n font-size: 16px;\n\n margin-right: 10px;\n}\n",".loader {\n display: inline-block;\n position: relative;\n width: 80px;\n height: 80px;\n}\n.loader:after {\n content: ' ';\n display: block;\n border-radius: 50%;\n width: 0;\n height: 0;\n margin: 8px;\n box-sizing: border-box;\n border: 32px solid #000;\n border-color: #000 transparent #000 transparent;\n animation: lds-hourglass 1.2s infinite;\n}\n@keyframes lds-hourglass {\n 0% {\n transform: rotate(0);\n animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n }\n 50% {\n transform: rotate(900deg);\n animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n 100% {\n transform: rotate(1800deg);\n }\n}\n",".input {\n border: 4px solid black;\n background: white;\n\n font-family: monospace;\n font-size: 16px;\n\n padding: 10px;\n margin-right: 30px;\n}\n"],"names":[],"version":3,"file":"index.2dc47d80.css.map"}
--------------------------------------------------------------------------------
/src/app/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react'
2 | import { Container, Header } from './components/Container'
3 | import { Image } from './components/Image'
4 | import { ImageGrid } from './components/ImageGrid'
5 | import { Button } from './components/Button'
6 | import { Swatch } from './components/Swatch'
7 |
8 | import FlipMove from 'react-flip-move'
9 | import * as Vibrant from 'node-vibrant/dist/vibrant.worker'
10 | import chroma from 'chroma-js'
11 | import { Loader } from './components/Loader'
12 | import { Input } from './components/Input'
13 |
14 | const CLIENT_ID = 'b082be41ba970bd'
15 | const CLIENT_SECRET = 'e1c2324dd6b4bac3587b1a38d9879a2a6f5abff7'
16 |
17 | const shuffle = (input) =>
18 | input
19 | .map((a) => ({ sort: Math.random(), value: a }))
20 | .sort((a, b) => a.sort - b.sort)
21 | .map((a) => a.value)
22 |
23 | const initializeState = async (albumHash) => {
24 | const response = await fetch(
25 | `https://api.imgur.com/3/album/${albumHash}/images`,
26 | {
27 | headers: {
28 | Authorization: `Client-Id ${CLIENT_ID}`,
29 | },
30 | }
31 | )
32 |
33 | const album = await response.json()
34 |
35 | return await Promise.all(
36 | album.data
37 | .map((img) => `https://i.imgur.com/${img.id}m.jpg`)
38 | .map((src) => {
39 | return Vibrant.from(src)
40 | .getPalette()
41 | .then((palette) => ({
42 | src: src,
43 | palette: palette,
44 | activeColor: 3,
45 | }))
46 | })
47 | )
48 | }
49 |
50 | const getActiveColor = (img) => Object.values(img.palette)[img.activeColor]
51 |
52 | export const App = () => {
53 | const [inputText, setInputText] = useState('')
54 | const [isChoosingRef, setChoosingRef] = useState(false)
55 | const [refColor, setRefColor] = useState('#FF0000')
56 | const [albumId, setAlbumId] = useState(null)
57 | const [isLoading, setLoading] = useState(false)
58 | const [state, setState] = useState([])
59 | const [reverseSort, setReverseSort] = useState(false)
60 |
61 | useEffect(() => {
62 | if (albumId !== null) {
63 | setLoading(true)
64 | initializeState(albumId).then((newState) => {
65 | setState(newState)
66 | setLoading(false)
67 | })
68 | }
69 | }, [albumId])
70 |
71 | const setActive = useCallback(
72 | (imageIndex, index) => {
73 | if (isChoosingRef) {
74 | const color = Object.values(state[imageIndex].palette)[index]
75 |
76 | const [r, g, b] = color.getRgb()
77 |
78 | setChoosingRef(false)
79 | setRefColor(`rgb(${r.toFixed(0)}, ${g.toFixed(0)}, ${b.toFixed(0)})`)
80 |
81 | return
82 | }
83 |
84 | setState((state) => {
85 | const newState = [...state]
86 |
87 | newState[imageIndex] = {
88 | ...state[imageIndex],
89 | activeColor: index,
90 | }
91 |
92 | return newState
93 | })
94 | },
95 | [state, setState, isChoosingRef]
96 | )
97 |
98 | const setRef = useCallback(() => {
99 | setChoosingRef((value) => !value)
100 | }, [setChoosingRef])
101 |
102 | if (albumId === null) {
103 | return (
104 |
105 |
106 | setInputText(e.target.value)}
110 | />
111 |
120 |
121 |
122 | )
123 | }
124 |
125 | const sortByColor = () => {
126 | setState((state) => {
127 | const newState = [...state]
128 |
129 | newState.sort((img1, img2) => {
130 | const color1 = chroma(getActiveColor(img1).getRgb())
131 | const color2 = chroma(getActiveColor(img2).getRgb())
132 |
133 | const result1 = chroma.deltaE(color1, refColor, 0.5, 2)
134 | const result2 = chroma.deltaE(color2, refColor, 0.5, 2)
135 |
136 | return reverseSort ? result1 - result2 : result2 - result1
137 | })
138 |
139 | return newState
140 | })
141 | }
142 |
143 | return (
144 |
145 |
164 |
165 | {isLoading ? : null}
166 |
167 |
168 |
169 | {state.map(({ src, palette, activeColor }, i) => (
170 | setActive(i, index)}
176 | />
177 | ))}
178 |
179 |
180 |
181 | )
182 | }
183 |
--------------------------------------------------------------------------------