├── .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 |
146 |

Sort Images By Color

147 |
148 |
149 | 150 | 151 |
152 | 153 | 154 | Reference color: 155 | 156 | setReverseSort((value) => !value)} 158 | value={reverseSort ? '<' : '>'} 159 | > 160 | Order: 161 | 162 |
163 |
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 | --------------------------------------------------------------------------------