· 5 years ago · Dec 01, 2020, 04:10 PM
1import { useState } from 'react';
2import { Link } from 'react-router-dom';
3
4import HomeIcon from '../../icons/HomeIcon';
5import SettingsIcon from '../../icons/SettingsIcon';
6import LogoIcon from '../../icons/LogoIcon';
7import AngleDoubleLine from '../../icons/AngleDoubleLine';
8import SidebarBtn from './SidebarBtn';
9import './styles.css';
10import '../../styles/tailwind.output.css';
11
12const buttons = [
13 {
14 icon: HomeIcon,
15 title: 'Dashboard',
16 subchapters: [
17 {
18 title: 'Campaigns',
19 linkTo: `/campaigns`,
20 },
21 ],
22 },
23 {
24 icon: SettingsIcon,
25 title: 'Settings',
26 subchapters: [
27 {
28 title: 'Campaigns',
29 linkTo: `/#`,
30 },
31 {
32 title: 'Companies',
33 linkTo: `/#`,
34 },
35 ],
36 },
37];
38
39const Sidebar = () => {
40 const [isOpen, setIsOpen] = useState(false);
41 const [isFixOpen, setIsFixOpen] = useState(false);
42 const [activeSection, setActiveSection] = useState(null);
43
44 const styles = {
45 sidebar: `h-screen ${
46 isOpen ? 'sidebar-opened' : 'sidebar-closed'
47 } border-r border-gray-400 flex flex-col`,
48 title: `text-3xl text-center text-custom-blue font-bold font-sans select-none`,
49 showMoreBtn: `cursor-pointer bg-sidebar-btn p-4`,
50 hideMenuBtn: `px-5 font-body text-sidebar-font-btn text-opacity-60 select-none`,
51 };
52
53 return (
54 <div className={styles.sidebar}>
55 <div
56 className="flex-1"
57 onMouseEnter={() => {
58 setIsOpen(true);
59 }}
60 onMouseLeave={() => {
61 if (!isFixOpen) {
62 setIsOpen(false);
63 }
64 }}
65 >
66 <Link to="/">
67 <div className="flex py-2 cursor-pointer">
68 <div className="px-5 py-2">
69 <LogoIcon />
70 </div>
71 {isOpen && (
72 <h1 className={styles.title}>
73 <span className="text-custom-red">de</span>dupe
74 </h1>
75 )}
76 </div>
77 </Link>
78
79 {buttons.map((item, index) => {
80 const isButtonActive = activeSection === item.title;
81 return (
82 <SidebarBtn
83 key={index}
84 aside={<item.icon opacity={!isButtonActive ? 0.6 : ''} />}
85 isActive={isButtonActive}
86 onClick={() => {
87 setIsOpen(true);
88 setActiveSection(isButtonActive ? null : item.title);
89 }}
90 isSidebarOpen={isOpen}
91 title={item.title}
92 subchapters={item.subchapters}
93 />
94 );
95 })}
96 </div>
97
98 <div
99 className={styles.showMoreBtn}
100 onClick={() => {
101 setIsOpen(!isOpen);
102 setIsFixOpen(!isOpen);
103 }}
104 onMouseEnter={() => {
105 if (isOpen) {
106 setIsOpen(true);
107 }
108 }}
109 >
110 <div className={`px-3 ${!isOpen ? 'transform rotate-180' : ''}`}>
111 <AngleDoubleLine />
112 {isOpen && <span className={styles.hideMenuBtn}>Hide menu</span>}
113 </div>
114 </div>
115 </div>
116 );
117};
118
119export default Sidebar;
120import ArrowIcon from '../../icons/ArrowIcon';
121
122import { Link } from 'react-router-dom';
123
124const SidebarBtn = ({
125 aside,
126 isSidebarOpen,
127 isActive,
128 onClick,
129 title,
130 subchapters,
131}) => {
132 const styles = {
133 title: `px-5 font-body text-sidebar-font-btn ${
134 !isActive && 'text-opacity-60'
135 } select-none`,
136 subchapter: `px-20 pb-5 cursor-pointer font-body text-15px text-sidebar-font-btn select-none`,
137 };
138
139 return (
140 <div className={isActive ? 'bg-sidebar-btn' : ''}>
141 <div className="my-5 mx-5 pt-5 pb-5" onClick={() => onClick(!isActive)}>
142 <div className="flex cursor-pointer menu-btn-height">
143 <div className="px-2">{aside}</div>
144 {isSidebarOpen && (
145 <>
146 <p className={styles.title}>{title}</p>
147 <span
148 className={`py-2 ml-auto ${isActive && 'transform rotate-180'}`}
149 >
150 <ArrowIcon />
151 </span>
152 </>
153 )}
154 </div>
155 </div>
156
157 <ul>
158 {isSidebarOpen &&
159 isActive &&
160 subchapters.map((item, index) => {
161 return (
162 <li key={index} className={styles.subchapter}>
163 <Link to={item.linkTo}>{item.title}</Link>
164 </li>
165 );
166 })}
167 </ul>
168 </div>
169 );
170};
171
172export default SidebarBtn;
173
174// <Link to={item.linkTo}>{item.title}</Link>
175.sidebar-closed {
176 width: 80px;
177}
178
179.sidebar-opened {
180 width: 250px;
181}
182
183.menu-btn-height {
184 height: 24px;
185}
186
187import React from 'react';
188
189const AngleDoubleLine = () => {
190 return (
191 <>
192 <svg className="inline-block" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
193 <path d="M12.94 4.66669C12.9405 4.75442 12.9237 4.8414 12.8905 4.92263C12.8573 5.00385 12.8085 5.07773 12.7467 5.14002L5.88667 12L12.7467 18.86C12.8559 18.9876 12.913 19.1516 12.9065 19.3194C12.9 19.4872 12.8304 19.6463 12.7117 19.7651C12.593 19.8838 12.4338 19.9533 12.266 19.9598C12.0982 19.9663 11.9342 19.9092 11.8067 19.8L4 12L11.8067 4.19335C11.9003 4.10162 12.0188 4.0395 12.1475 4.01476C12.2762 3.99002 12.4093 4.00377 12.5303 4.05427C12.6512 4.10478 12.7546 4.18981 12.8274 4.29873C12.9003 4.40765 12.9395 4.53564 12.94 4.66669Z" fill="#1E2F59" fillOpacity="0.6"/>
194 <path d="M20.2733 4.66668C20.2738 4.75442 20.257 4.8414 20.2239 4.92262C20.1907 5.00385 20.1418 5.07772 20.08 5.14002L13.22 12L20.08 18.86C20.1892 18.9876 20.2463 19.1516 20.2398 19.3194C20.2333 19.4872 20.1638 19.6463 20.0451 19.7651C19.9263 19.8838 19.7672 19.9533 19.5994 19.9598C19.4316 19.9663 19.2675 19.9092 19.14 19.8L11.3333 12L19.14 4.19335C19.2336 4.10161 19.3522 4.03949 19.4808 4.01476C19.6095 3.99002 19.7427 4.00376 19.8636 4.05427C19.9845 4.10478 20.0879 4.1898 20.1608 4.29873C20.2336 4.40765 20.2728 4.53563 20.2733 4.66668Z" fill="#1E2F59" fillOpacity="0.6"/>
195 </svg>
196 </>
197 );
198};
199
200export default AngleDoubleLine;
201import React from 'react';
202
203const ArrowIcon = () => {
204 return (
205 <svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg">
206 <path d="M11.8 1.73605C11.8004 1.85556 11.774 1.97365 11.7228 2.08163C11.6716 2.18961 11.5968 2.28475 11.504 2.36004L6.70402 6.22402C6.56087 6.34168 6.38132 6.40601 6.19602 6.40601C6.01073 6.40601 5.83117 6.34168 5.68803 6.22402L0.88806 2.22404C0.724688 2.08825 0.621949 1.89313 0.602445 1.68159C0.582942 1.47005 0.648271 1.25942 0.784061 1.09605C0.919851 0.932678 1.11498 0.829939 1.32652 0.810436C1.53806 0.790932 1.74868 0.856261 1.91205 0.992051L6.20002 4.56803L10.488 1.11205C10.6054 1.01422 10.7484 0.952084 10.9001 0.93298C11.0517 0.913877 11.2056 0.93861 11.3436 1.00425C11.4817 1.0699 11.598 1.17371 11.6789 1.30339C11.7597 1.43308 11.8018 1.58322 11.8 1.73605Z" fill="#1E2F59" fillOpacity="0.6"/>
207 </svg>
208 );
209};
210
211export default ArrowIcon;
212const BackArrowIcon = () => {
213 return (
214 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline-block">
215 <path d="M10.2732 4.94L7.21986 8L10.2732 11.06L9.33319 12L5.33319 8L9.33319 4L10.2732 4.94Z" fill="#69788D"/>
216 </svg>
217 );
218};
219
220export default BackArrowIcon;
221const BinIcon = () => {
222 return (
223 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline-block">
224 <path d="M4.40234 1.39453H4.26562C4.34082 1.39453 4.40234 1.33301 4.40234 1.25781V1.39453H9.59766V1.25781C9.59766 1.33301 9.65918 1.39453 9.73438 1.39453H9.59766V2.625H10.8281V1.25781C10.8281 0.654541 10.3376 0.164062 9.73438 0.164062H4.26562C3.66235 0.164062 3.17188 0.654541 3.17188 1.25781V2.625H4.40234V1.39453ZM13.0156 2.625H0.984375C0.681885 2.625 0.4375 2.86938 0.4375 3.17188V3.71875C0.4375 3.79395 0.499023 3.85547 0.574219 3.85547H1.60645L2.02856 12.7935C2.05591 13.3762 2.53784 13.8359 3.12061 13.8359H10.8794C11.4639 13.8359 11.9441 13.3779 11.9714 12.7935L12.3936 3.85547H13.4258C13.501 3.85547 13.5625 3.79395 13.5625 3.71875V3.17188C13.5625 2.86938 13.3181 2.625 13.0156 2.625ZM10.7478 12.6055H3.2522L2.83862 3.85547H11.1614L10.7478 12.6055Z" fill="#1E2F59" fillOpacity="0.6"/>
225 </svg>
226 );
227};
228
229export default BinIcon;
230const CrossIcon = () => {
231 return (
232 <svg
233 width="13"
234 height="13"
235 viewBox="0 0 13 13"
236 fill="none"
237 xmlns="http://www.w3.org/2000/svg"
238 >
239 <path
240 d="M7.94015 5.99993L12.8001 1.13993C12.9094 1.0124 12.9664 0.848345 12.96 0.680561C12.9535 0.512777 12.8839 0.353618 12.7652 0.234888C12.6465 0.116158 12.4873 0.0466031 12.3195 0.0401224C12.1517 0.0336416 11.9877 0.0907126 11.8601 0.19993L7.00014 5.05993L2.14015 0.193263C2.01261 0.0840458 1.84856 0.0269754 1.68078 0.0334562C1.51299 0.0399369 1.35383 0.109492 1.2351 0.228221C1.11637 0.346951 1.04682 0.50611 1.04034 0.673894C1.03386 0.841678 1.09093 1.00573 1.20015 1.13326L6.06015 5.99993L1.19348 10.8599C1.12369 10.9197 1.06701 10.9932 1.02699 11.0759C0.986979 11.1587 0.964492 11.2487 0.960946 11.3406C0.9574 11.4324 0.97287 11.5239 1.00639 11.6095C1.0399 11.695 1.09074 11.7727 1.15571 11.8377C1.22068 11.9027 1.29838 11.9535 1.38393 11.987C1.46948 12.0205 1.56103 12.036 1.65285 12.0325C1.74466 12.0289 1.83475 12.0064 1.91746 11.9664C2.00017 11.9264 2.07371 11.8697 2.13348 11.7999L7.00014 6.93993L11.8601 11.7999C11.9877 11.9091 12.1517 11.9662 12.3195 11.9597C12.4873 11.9533 12.6465 11.8837 12.7652 11.765C12.8839 11.6462 12.9535 11.4871 12.96 11.3193C12.9664 11.1515 12.9094 10.9875 12.8001 10.8599L7.94015 5.99993Z"
241 fill="#1E2F59"
242 />
243 </svg>
244 );
245};
246
247export default CrossIcon;
248import React from 'react';
249
250const DeletePopUpIcon = () => {
251 return (
252 <svg
253 width="36"
254 height="38"
255 viewBox="0 0 36 38"
256 fill="none"
257 xmlns="http://www.w3.org/2000/svg"
258 >
259 <path
260 d="M35.2174 6.08H26.6095V3.8C26.6083 2.79253 26.1956 1.82666 25.4621 1.11427C24.7285 0.401884 23.7339 0.00115659 22.6964 0H13.3051C12.2677 0.00115659 11.2731 0.401884 10.5395 1.11427C9.80593 1.82666 9.39328 2.79253 9.39209 3.8V6.08H0.782609C0.575048 6.08 0.375989 6.16007 0.229221 6.3026C0.0824533 6.44513 0 6.63844 0 6.84C0 7.04156 0.0824533 7.23487 0.229221 7.3774C0.375989 7.51993 0.575048 7.6 0.782609 7.6H3.13043V35.72C3.13116 36.3245 3.37875 36.904 3.8189 37.3314C4.25904 37.7589 4.8558 37.9993 5.47826 38H30.5217C31.1442 37.9993 31.741 37.7589 32.1811 37.3314C32.6212 36.904 32.8688 36.3245 32.8696 35.72V7.6H35.2174C35.425 7.6 35.624 7.51993 35.7708 7.3774C35.9175 7.23487 36 7.04156 36 6.84C36 6.63844 35.9175 6.44513 35.7708 6.3026C35.624 6.16007 35.425 6.08 35.2174 6.08ZM10.9573 3.8C10.958 3.19552 11.2056 2.616 11.6458 2.18857C12.0859 1.76114 12.6827 1.5207 13.3051 1.52H22.6964C23.3189 1.5207 23.9157 1.76114 24.3558 2.18857C24.7959 2.616 25.0435 3.19552 25.0443 3.8V6.08H10.9573V3.8ZM31.3043 35.72C31.3041 35.9215 31.2216 36.1147 31.0748 36.2571C30.9281 36.3996 30.7292 36.4797 30.5217 36.48H5.47826C5.27078 36.4797 5.07187 36.3996 4.92516 36.2571C4.77845 36.1147 4.69591 35.9215 4.69565 35.72V7.6H31.3043V35.72ZM14.0877 15.96V28.12C14.0877 28.3216 14.0053 28.5149 13.8585 28.6574C13.7118 28.7999 13.5127 28.88 13.3051 28.88C13.0976 28.88 12.8985 28.7999 12.7517 28.6574C12.605 28.5149 12.5225 28.3216 12.5225 28.12V15.96C12.5225 15.7584 12.605 15.5651 12.7517 15.4226C12.8985 15.2801 13.0976 15.2 13.3051 15.2C13.5127 15.2 13.7118 15.2801 13.8585 15.4226C14.0053 15.5651 14.0877 15.7584 14.0877 15.96ZM23.479 15.96V28.12C23.479 28.3216 23.3966 28.5149 23.2498 28.6574C23.1031 28.7999 22.904 28.88 22.6964 28.88C22.4889 28.88 22.2898 28.7999 22.143 28.6574C21.9963 28.5149 21.9138 28.3216 21.9138 28.12V15.96C21.9138 15.7584 21.9963 15.5651 22.143 15.4226C22.2898 15.2801 22.4889 15.2 22.6964 15.2C22.904 15.2 23.1031 15.2801 23.2498 15.4226C23.3966 15.5651 23.479 15.7584 23.479 15.96Z"
261 fill="#E20D21"
262 />
263 </svg>
264 );
265};
266
267export default DeletePopUpIcon;
268const DollarIcon = () => {
269 return (
270 <svg
271 width="9"
272 height="14"
273 viewBox="0 0 9 14"
274 fill="none"
275 xmlns="http://www.w3.org/2000/svg"
276 >
277 <path
278 d="M4.04688 13.2727H4.98153L4.98651 12.1541C7.08949 12 8.29261 10.7969 8.29261 9.18608C8.29261 7.35156 6.65696 6.64062 5.36435 6.32244L5.00142 6.22798L5.01136 3.03622C5.94105 3.16051 6.57741 3.67756 6.67188 4.4929H8.15341C8.11364 2.97656 6.8608 1.84304 5.01634 1.69389L5.02131 0.545454H4.08665L4.08168 1.70383C2.29688 1.88281 0.989347 2.99645 0.989347 4.60227C0.989347 6.01918 2.00355 6.84943 3.6392 7.2919L4.06676 7.40625L4.05682 10.7919C3.08736 10.6726 2.33665 10.1456 2.2571 9.18608H0.715909C0.810369 10.9013 2.0483 12.005 4.05185 12.1541L4.04688 13.2727ZM4.99148 10.7919L5.00142 7.66477C6.01065 7.94318 6.77131 8.31108 6.77131 9.17116C6.77131 10.0412 6.04545 10.6527 4.99148 10.7919ZM4.07173 5.9794C3.33097 5.76065 2.51562 5.3679 2.5206 4.51278C2.5206 3.76705 3.11222 3.18537 4.0767 3.04119L4.07173 5.9794Z"
279 fill="#1E2F59"
280 />
281 </svg>
282 );
283};
284
285export default DollarIcon;
286const DownloadIcon = () => {
287 return (
288 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline-block">
289 <path d="M14.75 10.25V13.25C14.75 13.6478 14.592 14.0294 14.3107 14.3107C14.0294 14.592 13.6478 14.75 13.25 14.75H2.75C2.35218 14.75 1.97064 14.592 1.68934 14.3107C1.40804 14.0294 1.25 13.6478 1.25 13.25V10.25" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
290 <path d="M4.25 6.5L8 10.25L11.75 6.5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
291 <path d="M8 10.25V1.25" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
292 </svg>
293
294 );
295};
296
297export default DownloadIcon;
298const EditIcon = () => {
299 return (
300 <svg
301 width="18"
302 height="18"
303 viewBox="0 0 18 18"
304 fill="none"
305 xmlns="http://www.w3.org/2000/svg"
306 >
307 <path
308 d="M8.25 3H3C2.60218 3 2.22064 3.15804 1.93934 3.43934C1.65804 3.72064 1.5 4.10218 1.5 4.5V15C1.5 15.3978 1.65804 15.7794 1.93934 16.0607C2.22064 16.342 2.60218 16.5 3 16.5H13.5C13.8978 16.5 14.2794 16.342 14.5607 16.0607C14.842 15.7794 15 15.3978 15 15V9.75"
309 stroke="#1E2F59"
310 strokeOpacity="0.6"
311 strokeWidth="1.2"
312 strokeLinecap="round"
313 strokeLinejoin="round"
314 />
315 <path
316 d="M13.875 1.87493C14.1734 1.57656 14.578 1.40894 15 1.40894C15.422 1.40894 15.8266 1.57656 16.125 1.87493C16.4234 2.17329 16.591 2.57797 16.591 2.99993C16.591 3.42188 16.4234 3.82656 16.125 4.12493L9 11.2499L6 11.9999L6.75 8.99992L13.875 1.87493Z"
317 stroke="#1E2F59"
318 strokeOpacity="0.6"
319 strokeWidth="1.2"
320 strokeLinecap="round"
321 strokeLinejoin="round"
322 />
323 </svg>
324 );
325};
326
327export default EditIcon;
328import React from 'react';
329
330const HomeIcon = ({ opacity }) => {
331 return (
332 <svg
333 width="22"
334 height="22"
335 viewBox="0 0 22 22"
336 fill="none"
337 xmlns="http://www.w3.org/2000/svg"
338 >
339 <path
340 d="M8.15722 19.7714V16.7047C8.1572 15.9246 8.79312 15.2908 9.58101 15.2856H12.4671C13.2587 15.2856 13.9005 15.9209 13.9005 16.7047V16.7047V19.7809C13.9003 20.4432 14.4343 20.9845 15.103 21H17.0271C18.9451 21 20.5 19.4607 20.5 17.5618V17.5618V8.83784C20.4898 8.09083 20.1355 7.38935 19.538 6.93303L12.9577 1.6853C11.8049 0.771566 10.1662 0.771566 9.01342 1.6853L2.46203 6.94256C1.86226 7.39702 1.50739 8.09967 1.5 8.84736V17.5618C1.5 19.4607 3.05488 21 4.97291 21H6.89696C7.58235 21 8.13797 20.4499 8.13797 19.7714V19.7714"
341 stroke="#1E2F59"
342 strokeOpacity={opacity}
343 strokeWidth="1.5"
344 strokeLinecap="round"
345 strokeLinejoin="round"
346 />
347 </svg>
348 );
349};
350
351export default HomeIcon;
352import React from 'react';
353
354const LogoIcon = () => {
355 return (
356 <svg width="35" height="30" viewBox="0 0 35 30" fill="none" xmlns="http://www.w3.org/2000/svg">
357 <path d="M17.5348 14.6378C14.2336 14.6378 11.5571 11.3633 11.5571 7.32409C11.5571 3.28479 12.436 0.0102539 17.5348 0.0102539C22.6339 0.0102539 23.5125 3.28479 23.5125 7.32409C23.5125 11.3633 20.8361 14.6378 17.5348 14.6378Z" fill="#E20D21"/>
358 <path d="M6.25741 25.5098C6.36799 18.4732 7.28032 16.4682 14.2614 15.199C14.2614 15.199 15.2443 16.4603 17.5348 16.4603C19.8253 16.4603 20.808 15.199 20.808 15.199C27.7129 16.4543 28.6809 18.4296 28.8081 25.2811C28.8184 25.8406 28.8232 25.87 28.8251 25.805C28.8247 25.9267 28.8242 26.1518 28.8242 26.5443C28.8242 26.5443 27.1621 29.9194 17.5347 29.9194C7.90726 29.9194 6.24499 26.5443 6.24499 26.5443C6.24499 26.2922 6.24481 26.1167 6.24463 25.9975C6.24652 26.0377 6.25039 25.9598 6.25741 25.5098Z" fill="#E20D21"/>
359 <path d="M9.23915 13.3319C6.55782 13.3319 4.38422 10.6724 4.38422 7.39184C4.38422 4.11115 5.09789 1.45166 9.23915 1.45166C9.93573 1.45166 10.5351 1.52716 11.0508 1.66809C10.0943 3.44435 9.96317 5.60181 9.96317 7.32396C9.96317 9.27387 10.4879 11.1355 11.4542 12.6769C10.79 13.0947 10.0374 13.3319 9.23915 13.3319Z" fill="#1E2F59"/>
360 <path d="M0.079977 22.1619C0.169951 16.4469 0.910798 14.8185 6.5808 13.7876C6.5808 13.7876 7.37896 14.8121 9.23927 14.8121C9.3162 14.8121 9.39078 14.8098 9.4642 14.8064C8.28221 15.3432 7.20693 16.0907 6.41507 17.1845C5.04611 19.0754 4.73138 21.6155 4.66642 25.3472C0.851777 24.5879 0.0699902 23.0022 0.0699902 23.0022C0.0699902 22.7956 0.0699873 22.6538 0.0696278 22.5569C0.0711575 22.591 0.0743084 22.5299 0.079977 22.1619Z" fill="#1E2F59"/>
361 <path d="M25.8306 13.3318C25.0322 13.3318 24.2798 13.0947 23.6153 12.6769C24.5816 11.1355 25.1064 9.27387 25.1064 7.32396C25.1064 5.60172 24.9753 3.44426 24.0189 1.6681C24.5345 1.52716 25.1338 1.45166 25.8306 1.45166C29.9718 1.45166 30.6853 4.11115 30.6853 7.39184C30.6853 10.6724 28.5117 13.3318 25.8306 13.3318Z" fill="#1E2F59"/>
362 <path d="M25.6056 14.8064C25.6788 14.8098 25.7534 14.8121 25.8305 14.8121C27.6908 14.8121 28.489 13.7876 28.489 13.7876C34.1588 14.8185 34.8999 16.4468 34.9897 22.1619C34.9955 22.53 34.9985 22.591 35 22.5568C34.9998 22.6537 34.9996 22.7955 34.9996 23.0021C34.9996 23.0021 34.2178 24.5878 30.4034 25.3471C30.3382 21.6155 30.0236 19.0754 28.6545 17.1844C27.8627 16.0908 26.7876 15.3432 25.6056 14.8064Z" fill="#1E2F59"/>
363 </svg>
364 );
365};
366
367export default LogoIcon;
368const PaginationArrowIcon = () => {
369 return (
370 <svg
371 width="6"
372 height="10"
373 viewBox="0 0 6 10"
374 fill="none"
375 xmlns="http://www.w3.org/2000/svg"
376 >
377 <path
378 d="M5.79971 1.10636C6.42812 0.51287 5.43313 -0.426818 4.80472 0.216126L0.196378 4.51891C-0.0654595 4.7662 -0.0654595 5.21131 0.196378 5.4586L4.80472 9.81084C5.43313 10.4043 6.42812 9.46464 5.79971 8.87115L1.71504 5.01348L5.79971 1.10636Z"
379 fill="#48658F"
380 />
381 </svg>
382 );
383};
384
385export default PaginationArrowIcon;
386const PlusIcon = () => {
387 return (
388 <svg
389 width="16"
390 height="16"
391 viewBox="0 0 15 15"
392 fill="none"
393 xmlns="http://www.w3.org/2000/svg"
394 className="inline-block"
395 >
396 <path
397 d="M12 5.14286H6.85714V0H5.14286V5.14286H0V6.85714H5.14286V12H6.85714V6.85714H12V5.14286Z"
398 fill="white"
399 />
400 </svg>
401 );
402};
403
404export default PlusIcon;
405const PlusIconGray = () => {
406 return (
407 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline-block">
408 <path d="M10 4.28571H5.71429V0H4.28571V4.28571H0V5.71429H4.28571V10H5.71429V5.71429H10V4.28571Z" fill="#1E2F59" fillOpacity="0.6"/>
409 </svg>
410 );
411};
412
413export default PlusIconGray;
414const ReportIcon = () => {
415 return (
416 <svg
417 width="16"
418 height="18"
419 viewBox="0 0 16 18"
420 fill="none"
421 xmlns="http://www.w3.org/2000/svg"
422 >
423 <path
424 d="M3.56372 10.2871H8.60247V11.5468H3.56372V10.2871Z"
425 fill="#1E2F59"
426 fillOpacity="0.6"
427 />
428 <path
429 d="M3.56372 7.1377H11.1218V8.39738H3.56372V7.1377Z"
430 fill="#1E2F59"
431 fillOpacity="0.6"
432 />
433 <path
434 d="M3.56372 13.4363H6.71294V14.696H3.56372V13.4363Z"
435 fill="#1E2F59"
436 fillOpacity="0.6"
437 />
438 <path
439 d="M13.0114 2.099H11.1218V1.46916C11.1218 1.13507 10.9891 0.814664 10.7529 0.578427C10.5166 0.342189 10.1962 0.209473 9.86215 0.209473H4.8234C4.48931 0.209473 4.1689 0.342189 3.93266 0.578427C3.69643 0.814664 3.56371 1.13507 3.56371 1.46916V2.099H1.67418C1.34009 2.099 1.01968 2.23172 0.783444 2.46796C0.547206 2.7042 0.41449 3.0246 0.41449 3.35869V16.5854C0.41449 16.9195 0.547206 17.2399 0.783444 17.4761C1.01968 17.7124 1.34009 17.8451 1.67418 17.8451H13.0114C13.3455 17.8451 13.6659 17.7124 13.9021 17.4761C14.1383 17.2399 14.2711 16.9195 14.2711 16.5854V3.35869C14.2711 3.0246 14.1383 2.7042 13.9021 2.46796C13.6659 2.23172 13.3455 2.099 13.0114 2.099ZM4.8234 1.46916H9.86215V3.98854H4.8234V1.46916ZM13.0114 16.5854H1.67418V3.35869H3.56371V5.24822H11.1218V3.35869H13.0114V16.5854Z"
440 fill="#1E2F59"
441 fillOpacity="0.6"
442 />
443 </svg>
444 );
445};
446
447export default ReportIcon;
448const SearchIcon = () => {
449 return (
450 <svg
451 width="16"
452 height="16"
453 viewBox="0 0 16 16"
454 fill="none"
455 xmlns="http://www.w3.org/2000/svg"
456 >
457 <path
458 d="M6.73684 13.4737C8.23156 13.4734 9.6832 12.973 10.8606 12.0522L14.5625 15.7541L15.7533 14.5634L12.0514 10.8615C12.9726 9.68393 13.4733 8.23195 13.4737 6.73684C13.4737 3.02232 10.4514 0 6.73684 0C3.02232 0 0 3.02232 0 6.73684C0 10.4514 3.02232 13.4737 6.73684 13.4737ZM6.73684 1.68421C9.52337 1.68421 11.7895 3.95032 11.7895 6.73684C11.7895 9.52337 9.52337 11.7895 6.73684 11.7895C3.95032 11.7895 1.68421 9.52337 1.68421 6.73684C1.68421 3.95032 3.95032 1.68421 6.73684 1.68421Z"
459 fill="#1E2F59"
460 fillOpacity="0.6"
461 />
462 </svg>
463 );
464};
465
466export default SearchIcon;
467import React from 'react';
468
469const SettingsIcon = ({ opacity }) => {
470 return (
471 <svg
472 width="21"
473 height="22"
474 viewBox="0 0 21 22"
475 fill="none"
476 xmlns="http://www.w3.org/2000/svg"
477 >
478 <path
479 fillRule="evenodd"
480 clipRule="evenodd"
481 d="M19.1738 6.88582L18.5514 5.80573C18.0248 4.89181 16.8579 4.57653 15.9427 5.10092V5.10092C15.5071 5.35755 14.9872 5.43036 14.4979 5.30329C14.0085 5.17623 13.5898 4.85972 13.334 4.42358C13.1695 4.14636 13.0811 3.8306 13.0777 3.50825V3.50825C13.0925 2.99143 12.8976 2.49061 12.5372 2.11988C12.1768 1.74914 11.6817 1.54007 11.1647 1.54028H9.91066C9.40412 1.54028 8.91847 1.74212 8.56116 2.10115C8.20384 2.46018 8.00433 2.9468 8.00677 3.45333V3.45333C7.99175 4.49913 7.13964 5.33902 6.09372 5.33891C5.77137 5.33556 5.45561 5.24715 5.17839 5.08262V5.08262C4.26322 4.55822 3.09627 4.8735 2.5697 5.78742L1.9015 6.88582C1.37557 7.7986 1.68656 8.96481 2.59716 9.49452V9.49452C3.18906 9.83625 3.55369 10.4678 3.55369 11.1513C3.55369 11.8347 3.18906 12.4663 2.59716 12.808V12.808C1.68771 13.3341 1.37638 14.4975 1.9015 15.4076V15.4076L2.53308 16.4968C2.7798 16.942 3.19376 17.2705 3.68335 17.4096C4.17294 17.5488 4.6978 17.4871 5.14178 17.2382V17.2382C5.57824 16.9835 6.09835 16.9138 6.5865 17.0444C7.07465 17.175 7.49039 17.4952 7.74132 17.9339C7.90585 18.2111 7.99426 18.5269 7.99761 18.8492V18.8492C7.99761 19.9058 8.85411 20.7622 9.91066 20.7622H11.1647C12.2176 20.7623 13.0727 19.9113 13.0777 18.8584V18.8584C13.0753 18.3502 13.276 17.8622 13.6353 17.5029C13.9946 17.1436 14.4826 16.9429 14.9907 16.9453C15.3123 16.9539 15.6268 17.042 15.9061 17.2016V17.2016C16.8189 17.7275 17.9851 17.4166 18.5148 16.506V16.506L19.1738 15.4076C19.4289 14.9697 19.4989 14.4482 19.3684 13.9586C19.2378 13.4689 18.9174 13.0516 18.4782 12.7989V12.7989C18.0389 12.5462 17.7185 12.1288 17.588 11.6391C17.4574 11.1495 17.5274 10.628 17.7825 10.1902C17.9484 9.90054 18.1885 9.6604 18.4782 9.49452V9.49452C19.3833 8.9651 19.6936 7.8057 19.1738 6.89497V6.89497V6.88582Z"
482 stroke="#1E2F59"
483 strokeOpacity={opacity}
484 strokeWidth="1.5"
485 strokeLinecap="round"
486 strokeLinejoin="round"
487 />
488 <ellipse
489 cx="10.5423"
490 cy="11.1513"
491 rx="2.63615"
492 ry="2.63616"
493 stroke="#1E2F59"
494 strokeOpacity={opacity}
495 strokeWidth="1.5"
496 strokeLinecap="round"
497 strokeLinejoin="round"
498 />
499 </svg>
500 );
501};
502
503export default SettingsIcon;
504import React from 'react';
505
506const SuccessPopUpIcon = () => {
507 return (
508 <svg
509 width="38"
510 height="38"
511 viewBox="0 0 38 38"
512 fill="none"
513 xmlns="http://www.w3.org/2000/svg"
514 >
515 <path
516 d="M27.9097 13.9152C27.9786 13.9874 28.0326 14.0725 28.0687 14.1656C28.1047 14.2587 28.1221 14.3579 28.1197 14.4577C28.1174 14.5575 28.0954 14.6559 28.0551 14.7472C28.0147 14.8385 27.9568 14.9209 27.8846 14.9899L16.7381 25.6299C16.5967 25.7648 16.4088 25.84 16.2134 25.84C16.018 25.84 15.8301 25.7648 15.6887 25.6299L10.1152 20.3099C9.97188 20.1701 9.88944 19.9795 9.88583 19.7794C9.88221 19.5792 9.9577 19.3857 10.0959 19.2409C10.2341 19.0961 10.4239 19.0117 10.624 19.0059C10.8241 19.0002 11.0183 19.0737 11.1646 19.2103L16.2135 24.0293L26.8352 13.8903C26.981 13.7512 27.1761 13.6756 27.3776 13.6803C27.5791 13.6849 27.7705 13.7694 27.9097 13.9152ZM38 19C38 22.7578 36.8857 26.4313 34.7979 29.5558C32.7102 32.6804 29.7428 35.1156 26.271 36.5537C22.7992 37.9918 18.9789 38.368 15.2933 37.6349C11.6077 36.9018 8.22218 35.0922 5.56498 32.435C2.90778 29.7778 1.09821 26.3923 0.365088 22.7067C-0.368031 19.0211 0.0082321 15.2008 1.4463 11.729C2.88436 8.25722 5.31964 5.28982 8.44417 3.20208C11.5687 1.11433 15.2422 0 19 0C24.0374 0.0056826 28.8668 2.00929 32.4288 5.57125C35.9907 9.1332 37.9943 13.9626 38 19ZM36.48 19C36.48 15.5428 35.4548 12.1632 33.5341 9.28863C31.6134 6.41406 28.8834 4.1736 25.6893 2.85058C22.4953 1.52757 18.9806 1.1814 15.5898 1.85587C12.199 2.53034 9.0844 4.19515 6.63978 6.63977C4.19516 9.08439 2.53035 12.199 1.85588 15.5898C1.18141 18.9806 1.52757 22.4952 2.85059 25.6893C4.17361 28.8834 6.41407 31.6134 9.28864 33.5341C12.1632 35.4548 15.5428 36.48 19 36.48C23.6344 36.4748 28.0774 34.6314 31.3545 31.3544C34.6315 28.0774 36.4748 23.6344 36.48 19Z"
517 fill="#03BD5B"
518 />
519 </svg>
520 );
521};
522
523export default SuccessPopUpIcon;
524import React from 'react';
525
526const WarningPopUpIcon = () => {
527 return (
528 <svg
529 width="36"
530 height="32"
531 viewBox="0 0 36 32"
532 fill="none"
533 xmlns="http://www.w3.org/2000/svg"
534 >
535 <path
536 d="M17.3307 19.3341V11.3338C17.3307 11.157 17.401 10.9874 17.526 10.8624C17.6511 10.7374 17.8206 10.6671 17.9975 10.6671C18.1743 10.6671 18.3439 10.7374 18.4689 10.8624C18.5939 10.9874 18.6642 11.157 18.6642 11.3338V19.3341C18.6642 19.5109 18.5939 19.6804 18.4689 19.8055C18.3439 19.9305 18.1743 20.0007 17.9975 20.0007C17.8206 20.0007 17.6511 19.9305 17.526 19.8055C17.401 19.6804 17.3307 19.5109 17.3307 19.3341ZM17.9975 24.3342C17.7997 24.3342 17.6063 24.3929 17.4419 24.5027C17.2774 24.6126 17.1492 24.7688 17.0735 24.9515C16.9978 25.1343 16.978 25.3354 17.0166 25.5293C17.0552 25.7233 17.1504 25.9015 17.2903 26.0414C17.4302 26.1812 17.6084 26.2765 17.8024 26.3151C17.9963 26.3536 18.1974 26.3338 18.3802 26.2581C18.5629 26.1825 18.7191 26.0543 18.829 25.8898C18.9389 25.7254 18.9975 25.532 18.9975 25.3342C18.9975 25.069 18.8922 24.8147 18.7046 24.6271C18.5171 24.4396 18.2627 24.3342 17.9975 24.3342ZM35.549 30.3316C35.2593 30.8411 34.839 31.2642 34.3314 31.5575C33.8239 31.8507 33.2474 32.0034 32.6612 31.9999H3.33372C2.74822 32 2.17304 31.8458 1.66605 31.5529C1.15907 31.2601 0.738162 30.8388 0.445691 30.3316C0.153221 29.8244 -0.000498298 29.2491 1.21349e-06 28.6637C0.000500725 28.0782 0.155201 27.5032 0.448537 26.9965L15.1123 1.66369C15.4051 1.15776 15.8258 0.737729 16.3322 0.445721C16.8386 0.153714 17.4129 0 17.9975 0C18.582 0 19.1563 0.153714 19.6627 0.445721C20.1691 0.737729 20.5898 1.15776 20.8826 1.66369L35.5462 26.9965C35.8429 27.502 35.9995 28.0775 36 28.6636C36.0005 29.2498 35.8448 29.8255 35.549 30.3316ZM34.3923 27.6643L19.7284 2.33154C19.5527 2.02799 19.3003 1.77597 18.9964 1.60076C18.6926 1.42556 18.348 1.33333 17.9973 1.33333C17.6466 1.33333 17.302 1.42556 16.9981 1.60076C16.6943 1.77597 16.4419 2.02799 16.2662 2.33154L1.60278 27.6643C1.42678 27.9683 1.33396 28.3133 1.33365 28.6646C1.33335 29.0159 1.42556 29.361 1.60103 29.6653C1.77649 29.9697 2.02901 30.2224 2.33317 30.3981C2.63734 30.5739 2.98243 30.6664 3.33372 30.6664H32.6612C33.0125 30.6664 33.3576 30.5739 33.6617 30.3981C33.9659 30.2224 34.2184 29.9697 34.3939 29.6653C34.5694 29.361 34.6616 29.0159 34.6613 28.6646C34.661 28.3133 34.5681 27.9683 34.3921 27.6643H34.3923Z"
537 fill="#E20D21"
538 />
539 </svg>
540 );
541};
542
543export default WarningPopUpIcon;
544import faker from 'faker';
545import range from 'lodash/range';
546
547const generateCampaigns = () => {
548 const campaigns = range(30).map(num => {
549 return {
550 id: num,
551 name: faker.random.word(),
552 createdAt: faker.date.between('2015-01-01', '2020-11-11').toString(),
553 };
554 });
555 return campaigns;
556};
557
558const campaigns = generateCampaigns();
559
560const generateAdvertizers = () => {
561 const advertizers = range(30).map(num => {
562 return {
563 id: num,
564 name: faker.random.word(),
565 campaignId: num,
566 };
567 });
568 return advertizers;
569};
570
571const advertizers = generateAdvertizers();
572
573const generateCreatives = () => {
574 const creatives = range(70).map(num => {
575 return {
576 id: num,
577 name: faker.random.word(),
578 campaignId: num % campaigns.length,
579 };
580 });
581 return creatives;
582};
583
584const creatives = generateCreatives();
585
586const generateDsps = () => {
587 const dsps = range(100).map(num => {
588 return {
589 id: num,
590 name: faker.random.word(),
591 creativeId: num % creatives.length,
592 status: faker.random.arrayElement(['Active', 'Dormant', 'Stopped']),
593 };
594 });
595 return dsps;
596};
597
598const dsps = generateDsps();
599
600const generateSsps = () => {
601 const ssps = range(150).map(num => {
602 return {
603 id: num,
604 name: faker.random.word(),
605 dspId: num % dsps.length,
606 };
607 });
608 return ssps;
609};
610
611const ssps = generateSsps();
612
613export { campaigns, advertizers, creatives, dsps, ssps };
614import { makeExecutableSchema } from '@graphql-tools/schema';
615import { find, filter } from 'lodash';
616import { campaigns, advertizers, creatives, dsps, ssps } from './generators';
617
618const typeDefs = `
619 type CampaignsConnection {
620 edges: [CampaignEdge!]!
621 }
622
623 type CampaignEdge {
624 node: Campaign!
625 }
626
627 enum DspStatus {
628 Active
629 Dormant
630 Stopped
631 }
632
633 type Campaign {
634 id: Int!
635 createdAt: String
636 name: String
637 advertizer: Advertizer
638 creatives(ids: [Int]): CreativesConnection
639 }
640
641 type CreativesConnection {
642 edges: [CreativeEdge!]!
643 }
644
645 type CreativeEdge {
646 node: Creative!
647 }
648
649 type Advertizer {
650 id: Int!
651 name: String
652 }
653
654 type Creative {
655 id: Int!
656 name: String
657 dsps(ids: [Int]): DspsConnection
658 }
659
660 type DspsConnection {
661 edges: [DspEdge!]!
662 }
663
664 type DspEdge {
665 node: Dsp!
666 }
667
668 type Dsp {
669 id: Int!
670 name: String
671 status: DspStatus
672 ssps(ids: [Int]): SspsConnection
673 }
674
675 type SspsConnection {
676 edges: [SspEdge!]!
677 }
678
679 type SspEdge {
680 node: Ssp!
681 }
682
683 type Ssp {
684 id: Int!
685 name: String
686 }
687
688 type AdvertizersConnection {
689 edges: [AdvertizerEdge!]!
690 }
691
692 type AdvertizerEdge {
693 node: Advertizer!
694 }
695
696 type Query {
697 campaigns: CampaignsConnection
698 campaign(id: ID!): Campaign
699 advertizers: AdvertizersConnection
700 dsps: DspsConnection
701 ssps: SspsConnection
702 }
703
704 type Mutation {
705 createCampaign: Campaign
706 }
707`;
708
709const resolvers = {
710 Query: {
711 campaigns: () => {
712 return {
713 edges: campaigns.map(campaign => {
714 return {
715 node: campaign,
716 };
717 }),
718 };
719 },
720 campaign: (_, { id }) => {
721 const result = campaigns.find(campaign => campaign.id.toString() === id);
722 return result;
723 },
724 advertizers: () => {
725 return {
726 edges: advertizers.map(advertizer => {
727 return {
728 node: advertizer,
729 };
730 }),
731 };
732 },
733 dsps: () => {
734 return {
735 edges: dsps.map(dsp => {
736 return {
737 node: dsp,
738 };
739 }),
740 };
741 },
742 ssps: () => {
743 return {
744 edges: ssps.map(ssp => {
745 return {
746 node: ssp,
747 };
748 }),
749 };
750 },
751 },
752 Campaign: {
753 advertizer: campaign => find(advertizers, { campaignId: campaign.id }),
754 creatives: (campaign, args) => {
755 const { ids } = args;
756 const filterObj = { campaignId: campaign.id };
757 if (ids) {
758 filterObj.id = ids[0];
759 }
760 return {
761 edges: filter(creatives, filterObj).map(creative => {
762 return {
763 node: creative,
764 };
765 }),
766 };
767 },
768 },
769 Creative: {
770 dsps: (creative, args) => {
771 const { ids } = args;
772 const filterObj = { creativeId: creative.id };
773 if (ids) {
774 filterObj.id = ids[0];
775 }
776 return {
777 edges: filter(dsps, filterObj).map(dsp => {
778 return {
779 node: dsp,
780 };
781 }),
782 };
783 },
784 },
785 Dsp: {
786 ssps: (dsp, args) => {
787 const { ids } = args;
788 const filterObj = { dspId: dsp.id };
789 if (ids) {
790 filterObj.id = ids[0];
791 }
792 return {
793 edges: filter(ssps, filterObj).map(ssp => {
794 return {
795 node: ssp,
796 };
797 }),
798 };
799 },
800 },
801};
802
803const schema = makeExecutableSchema({
804 typeDefs,
805 resolvers,
806});
807
808export default schema;
809import { useState, Fragment } from 'react';
810import range from 'lodash/range';
811import Select from 'react-select';
812import { Link } from 'react-router-dom';
813import { useHistory } from 'react-router';
814
815import { useGetCampaignsQuery } from './queries';
816import { formatCampaignsResponse } from './helpers';
817
818import ReportIcon from '../../icons/ReportIcon';
819import EditIcon from '../../icons/EditIcon';
820import PlusIcon from '../../icons/PlusIcon';
821import SearchIcon from '../../icons/SearchIcon';
822import PaginationArrowIcon from '../../icons/PaginationArrowIcon';
823
824import { selectStyles, selectRowsStyles } from '../selectStyle';
825
826import './styles.css';
827
828const filterByActivityOptions = [
829 { value: 'all', label: 'All' },
830 { value: 'active', label: 'Active', color: '#03bd5b' },
831 { value: 'dormant', label: 'Dormant', color: '#f5a623' },
832 { value: 'stopped', label: 'Stopped', color: '#e20d21' },
833];
834
835const sortByDateOptions = [
836 { value: 'oldest', label: 'Oldest' },
837 { value: 'newest', label: 'Newest' },
838];
839
840const rowsPerPageOptions = [
841 { value: '10', label: 10 },
842 { value: '20', label: 20 },
843 { value: '30', label: 30 },
844];
845
846const CampaignsTable = () => {
847 const { data } = useGetCampaignsQuery();
848 const [sortByDate, setSortByDate] = useState('newest');
849 const [filterByType, setFilterByType] = useState('Active');
850 const [searchTerm, setSearchTerm] = useState('');
851 const [rowsPerPage, setRowsPerPage] = useState(10);
852 let [currentPage, setCurrentPage] = useState(1);
853 const history = useHistory();
854
855 const styles = {
856 tableWrapper: `align-middle inline-block min-w-full border-b border-gray-200 bg-table border border-table-border rounded-lg`,
857 table: `w-full divide-y divide-gray-200`,
858
859 tableTheadTr: `border-b border-table-border text-table-list-font text-left text-12px font-body tracking-wider select-none`,
860 tableTh: `px-6 py-3 whitespace-no-wrap`,
861
862 tableTbodyTr: `border-b border-table-border whitespace-no-wrap text-13px text-custom-blue font-body hover:shadow-md hover:bg-white`,
863
864 ellipse: status =>
865 `my-4 mx-6 ellipse ${
866 status === 'Active'
867 ? 'bg-custom-green'
868 : status === 'Dormant'
869 ? 'bg-custom-yellow'
870 : 'bg-custom-red'
871 } rounded-full`,
872
873 createCampaignBtn: `whitespace-no-wrap bg-custom-red text-white py-2 px-4 rounded-lg m-5 font-body text-14px select-none`,
874 searchField: `border rounded-lg py-2 px-3 pl-10 placeholder-custom-blue placeholder-opacity-50 font-body leading-tight focus:outline-none select-none`,
875
876 selectLabel: `pr-3 text-14px font-body text-table-list-font select-none whitespace-no-wrap`,
877 };
878
879 const handleSortByDateChange = event => {
880 setSortByDate(event.value);
881 setCurrentPage(1);
882 };
883 const handleFilterByStatusChange = event => {
884 setFilterByType(event.label);
885 setCurrentPage(1);
886 };
887 const handleSearchTermChange = event => {
888 setSearchTerm(event.target.value);
889 setCurrentPage(1);
890 };
891
892 const getDisplayedCampaigns = () => {
893 return rows
894 .filter(campaign => {
895 return (
896 campaign.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
897 campaign.advertizer.name
898 .toLowerCase()
899 .includes(searchTerm.toLowerCase())
900 );
901 })
902 .filter(campaign => {
903 if (filterByType === 'All') {
904 return true;
905 } else {
906 return campaign.creatives.some(creative => {
907 return creative.dsps.some(dsp => {
908 return dsp.status === filterByType;
909 });
910 });
911 }
912 })
913 .map(campaign => {
914 if (filterByType === 'All') {
915 return campaign;
916 }
917 const creatives = campaign.creatives
918 .filter(creative => {
919 return creative.dsps.some(dsp => {
920 return dsp.status === filterByType;
921 });
922 })
923 .map(creative => {
924 const dsps = creative.dsps.filter(
925 dsp => dsp.status === filterByType
926 );
927 return {
928 ...creative,
929 dsps,
930 };
931 });
932
933 return {
934 ...campaign,
935 creatives,
936 };
937 })
938 .sort((a, b) =>
939 sortByDate === 'newest'
940 ? a.createdAt - b.createdAt
941 : b.createdAt - a.createdAt
942 );
943 };
944
945 const rows = formatCampaignsResponse(data);
946 const displayedCampaigns = getDisplayedCampaigns(rows);
947
948 const handleRowsPerPage = event => {
949 setRowsPerPage(event.label);
950 setCurrentPage(1);
951 };
952
953 const to = currentPage * rowsPerPage;
954 const from = to - rowsPerPage;
955 const currentRows = displayedCampaigns.slice(from, to);
956
957 const paginate = direction => setCurrentPage(currentPage + direction);
958
959 return (
960 <div className="bg-main p-12 overflow-x-auto flex-grow">
961 <div className={styles.tableWrapper}>
962 <div className="flex">
963 <Link to="/campaigns/new" className={styles.createCampaignBtn}>
964 <PlusIcon />
965 <span className="pl-2 whitespace-no-wrap">Create campaign</span>
966 </Link>
967
968 <div className="relative my-auto">
969 <div className="absolute pl-3 pt-3">
970 <SearchIcon />
971 </div>
972 <input
973 className={styles.searchField}
974 type="text"
975 placeholder="Search..."
976 onChange={handleSearchTermChange}
977 />
978 </div>
979
980 <div className="relative my-auto pl-5 flex items-center">
981 <label htmlFor="filter" className={styles.selectLabel}>
982 Filter by
983 </label>
984 <div className="inline-block w-40">
985 <Select
986 name="filter"
987 id="filter"
988 theme={theme => ({
989 ...theme,
990 colors: { primary25: '#f5f4f4' },
991 })}
992 options={filterByActivityOptions}
993 styles={selectStyles}
994 defaultValue={filterByActivityOptions[1]}
995 components={{ IndicatorSeparator: null }}
996 onChange={handleFilterByStatusChange}
997 />
998 </div>
999 </div>
1000
1001 <div className="relative my-auto pl-32 flex items-center">
1002 <label htmlFor="sort" className={styles.selectLabel}>
1003 Sort by
1004 </label>
1005 <div className="inline-block w-40">
1006 <Select
1007 name="sort"
1008 id="sort"
1009 theme={theme => ({
1010 ...theme,
1011 colors: { primary25: '#f5f4f4' },
1012 })}
1013 options={sortByDateOptions}
1014 styles={selectStyles}
1015 defaultValue={sortByDateOptions[1]}
1016 components={{ IndicatorSeparator: null }}
1017 onChange={handleSortByDateChange}
1018 />
1019 </div>
1020 </div>
1021 </div>
1022
1023 <table className={styles.table}>
1024 <thead>
1025 <tr className={styles.tableTheadTr}>
1026 <th className={styles.tableTh}>Campaign Name</th>
1027 <th className={styles.tableTh}>Advertiser</th>
1028 <th className={styles.tableTh}>Creative Group</th>
1029 <th className={styles.tableTh}>DSP</th>
1030 <th className={styles.tableTh}>SSP</th>
1031 <th className={styles.tableTh}>Live status</th>
1032 <th className={styles.tableTh} />
1033 </tr>
1034 </thead>
1035
1036 <tbody className="align-top">
1037 {currentRows.map((campaign, index) => {
1038 return (
1039 <tr key={index} className={styles.tableTbodyTr}>
1040 <td className="px-6 py-5">
1041 <div
1042 className="flex"
1043 onClick={() =>
1044 history.push(`campaigns/${campaign.id}/edit`)
1045 }
1046 >
1047 <p className="mr-3 hover:underline cursor-pointer">
1048 {campaign.name}
1049 </p>{' '}
1050 <EditIcon />
1051 </div>
1052 </td>
1053
1054 <td className="px-6 py-5 max-w-xs/2">
1055 <p className="truncate hover:underline cursor-pointer">
1056 {campaign.advertizer.name}
1057 </p>
1058 </td>
1059
1060 <td className="px-6 py-4 max-w-xs/2">
1061 {campaign.creatives.map((creative, index) => {
1062 const dspsNumber = creative.dsps.length;
1063 return (
1064 <Fragment key={index}>
1065 <p className="pt-1 pb-1 truncate">{creative.name}</p>
1066 {range(dspsNumber - 1).map(num => {
1067 return (
1068 <p key={num} className="pt-1 pb-1 opacity-0">
1069 x
1070 </p>
1071 );
1072 })}
1073 </Fragment>
1074 );
1075 })}
1076 </td>
1077
1078 <td className="px-6 py-4 max-w-xs/2">
1079 {campaign.creatives.flatMap((creative, index) => {
1080 return creative.dsps.map((dsp, dspIndex) => {
1081 return (
1082 <p
1083 key={index + dspIndex + dsp.name}
1084 className="pt-1 pb-1 truncate"
1085 >
1086 {dsp.name}
1087 </p>
1088 );
1089 });
1090 })}
1091 </td>
1092
1093 <td className="px-6 py-4 max-w-xs/2">
1094 {campaign.creatives.flatMap((creative, index) => {
1095 return creative.dsps.map((dsp, dspIndex) => {
1096 const ssps = dsp.ssps.map(ssp => ssp.name);
1097 return (
1098 <p
1099 key={index + dspIndex + dsp.name}
1100 className="pt-1 pb-1 truncate"
1101 >
1102 {ssps.join(', ')}
1103 </p>
1104 );
1105 });
1106 })}
1107 </td>
1108
1109 <td className="px-6 py-2 max-w-xs/2">
1110 {campaign.creatives.flatMap((creative, index) => {
1111 return creative.dsps.map((dsp, dspIndex) => {
1112 return (
1113 <div
1114 key={index + dspIndex + dsp.name}
1115 className={styles.ellipse(dsp.status)}
1116 />
1117 );
1118 });
1119 })}
1120 </td>
1121
1122 <td className="px-6 py-4 max-w-xs/2">
1123 {campaign.creatives.flatMap((creative, index) => {
1124 return creative.dsps.map((dsp, dspIndex) => {
1125 return (
1126 <div
1127 key={index + dspIndex + dsp.name}
1128 className="p-1"
1129 >
1130 <ReportIcon />
1131 </div>
1132 );
1133 });
1134 })}
1135 </td>
1136 </tr>
1137 );
1138 })}
1139 </tbody>
1140 </table>
1141
1142 <div className="flex justify-end relative my-auto pl-5 mr-5">
1143 <label
1144 htmlFor="sort"
1145 className="py-3 text-12px text-table-list-font font-body"
1146 >
1147 Rows per page:
1148 </label>
1149 <div className="w-16">
1150 <Select
1151 name="rows"
1152 theme={theme => ({
1153 ...theme,
1154 colors: { primary25: '#f5f4f4' },
1155 })}
1156 options={rowsPerPageOptions}
1157 styles={selectRowsStyles}
1158 defaultValue={rowsPerPageOptions[0]}
1159 components={{ IndicatorSeparator: null }}
1160 onChange={handleRowsPerPage}
1161 />
1162 </div>
1163
1164 <p className="py-3 pl-8 text-12px text-table-list-font font-body">
1165 {from + 1}-
1166 {to < displayedCampaigns.length ? to : displayedCampaigns.length} of{' '}
1167 {displayedCampaigns.length}
1168 </p>
1169
1170 <div className="flex justify-between w-24 py-4 pl-10">
1171 <div
1172 onClick={() => currentPage > 1 && paginate(-1)}
1173 className="cursor-pointer"
1174 >
1175 <PaginationArrowIcon />
1176 </div>
1177
1178 <span className="transform rotate-180">
1179 <div
1180 onClick={() => to < displayedCampaigns.length && paginate(1)}
1181 className="cursor-pointer"
1182 >
1183 <PaginationArrowIcon />
1184 </div>
1185 </span>
1186 </div>
1187 </div>
1188
1189 <br />
1190 <br />
1191 <br />
1192 <br />
1193 <br />
1194 </div>
1195 </div>
1196 );
1197};
1198
1199export default CampaignsTable;
1200import get from 'lodash/get';
1201
1202export const formatCampaign = campaign => {
1203 const { name, advertizer, creatives, createdAt, id } = campaign;
1204 return {
1205 name,
1206 id,
1207 advertizer: {
1208 id: advertizer.id,
1209 name: advertizer.name,
1210 },
1211 createdAt: new Date(createdAt),
1212 creatives: (get(creatives, 'edges') || []).map(creativeEdge => {
1213 const { node } = creativeEdge;
1214 const { name, id, dsps } = node;
1215 return {
1216 id,
1217 name,
1218 dsps: (get(dsps, 'edges') || []).map(dspEdge => {
1219 const { node } = dspEdge;
1220 const { name, id, ssps, status } = node;
1221 return {
1222 name,
1223 id,
1224 status,
1225 ssps: (get(ssps, 'edges') || []).map(sspEdge => {
1226 const { node } = sspEdge;
1227 const { name, id } = node;
1228 return {
1229 name,
1230 id,
1231 fromServer: true,
1232 };
1233 }),
1234 };
1235 }),
1236 };
1237 }),
1238 };
1239};
1240
1241export const formatCampaignResponse = data => {
1242 if (!data || !data.campaign) {
1243 return {};
1244 }
1245 return formatCampaign(data.campaign);
1246};
1247
1248export const formatCampaignsResponse = data => {
1249 const formattedData = (get(data, 'campaigns.edges') || []).map(edge => {
1250 const { node } = edge;
1251 return formatCampaign(node);
1252 });
1253
1254 return formattedData;
1255};
1256import CampaignsTable from './CampaignsTable';
1257
1258const CampaignListPage = () => {
1259 const styles = {
1260 nav: `h-16 border-b border-gray-400`,
1261 navTitle: `text-3xl text-custom-blue px-5 py-2 font-bold font-body select-none`,
1262 };
1263
1264 return (
1265 <div className="flex flex-col h-full">
1266 <div className={styles.nav}>
1267 <p className={styles.navTitle}>Campaigns</p>
1268 </div>
1269
1270 <CampaignsTable />
1271 </div>
1272 );
1273};
1274
1275export default CampaignListPage;
1276import { gql, useQuery } from '@apollo/client';
1277
1278const GET_CAMPAIGNS = gql`
1279 query GetCampaigns {
1280 campaigns {
1281 edges {
1282 node {
1283 id
1284 name
1285 advertizer {
1286 name
1287 }
1288 createdAt
1289 creatives {
1290 edges {
1291 node {
1292 id
1293 name
1294 dsps {
1295 edges {
1296 node {
1297 id
1298 name
1299 status
1300 ssps {
1301 edges {
1302 node {
1303 id
1304 name
1305 }
1306 }
1307 }
1308 }
1309 }
1310 }
1311 }
1312 }
1313 }
1314 }
1315 }
1316 }
1317 }
1318`;
1319
1320export const useGetCampaignsQuery = () => {
1321 return useQuery(GET_CAMPAIGNS);
1322};
1323ul {
1324 padding-left: 20px;
1325}
1326
1327.list {
1328 font-size: 12px;
1329}
1330
1331.row {
1332 font-size: 13px;
1333}
1334
1335.ellipse {
1336 height: 10px;
1337 width: 10px;
1338}
1339import Slider from 'rc-slider';
1340import 'rc-slider/assets/index.css';
1341import { useFormContext, Controller } from 'react-hook-form';
1342
1343const styles = {
1344 tableWrapper: `h-auto bg-table border border-table-border`,
1345
1346 table: `min-w-full divide-y divide-table-border py-4`,
1347 label: `m-4 font-body text-table-list-font text-13px text-left`,
1348
1349 sliderValue: `font-body select-none text-custom-blue text-13px ml-2 -mt-1`,
1350 radioLabel: `font-body select-none text-table-list-font text-13px my-auto`,
1351 checkbox: `form-checkbox h-5 w-5 rounded text-blue-900 border-gray-500`,
1352 radioBtn: `my-auto mr-2 form-radio h-5 w-5 text-blue-900 border-gray-500`,
1353};
1354
1355const AlertsSlider = props => {
1356 return (
1357 <Slider
1358 {...props}
1359 min={0}
1360 dotStyle={{ opacity: '0' }}
1361 railStyle={{ backgroundColor: '#1e2f59' }}
1362 trackStyle={{ backgroundColor: '#1e2f59' }}
1363 handleStyle={{
1364 backgroundColor: '#1e2f59',
1365 borderColor: '#fff',
1366 height: 20,
1367 width: 20,
1368 marginTop: -8,
1369 }}
1370 />
1371 );
1372};
1373
1374const setMarks = (start, middle, end) => {
1375 const marks = {};
1376 marks[start] = { style: { marginTop: '-40px' }, label: start };
1377 marks[middle] = { style: { marginTop: '-40px' }, label: middle };
1378 marks[end] = { style: { marginTop: '-40px' }, label: end };
1379
1380 return marks;
1381};
1382
1383const AlertsThreshold = () => {
1384 const formData = useFormContext();
1385 const { register, getValues, setValue, control, watch } = formData;
1386 const sliderValue = watch([
1387 'sliderUntrustedDomains',
1388 'sliderAppDuplications',
1389 'sliderAdSessionExclusivity',
1390 'sliderBidRisk',
1391 'sliderClickProxies',
1392 ]);
1393
1394 const setCheckboxValues = () => {
1395 const allAlertsValue = getValues('allAlerts');
1396 setValue('checkboxUntrustedDomains', allAlertsValue);
1397 setValue('checkboxAppDuplications', allAlertsValue);
1398 setValue('checkboxAdSessionExclusivity', allAlertsValue);
1399 setValue('checkboxBidRisk', allAlertsValue);
1400 setValue('checkboxClickProxies', allAlertsValue);
1401 };
1402
1403 return (
1404 <div className={styles.tableWrapper}>
1405 <table className={styles.table}>
1406 <thead>
1407 <tr>
1408 <th className="w-1/3">
1409 <label
1410 htmlFor="allAlerts"
1411 className={`${styles.label} flex cursor-pointer`}
1412 >
1413 <input
1414 name="allAlerts"
1415 ref={register}
1416 id="allAlerts"
1417 className={`my-auto ${styles.checkbox}`}
1418 type="checkbox"
1419 onClick={setCheckboxValues}
1420 />
1421 <span className="pl-4 my-auto">All alerts</span>
1422 </label>
1423 </th>
1424
1425 <th className={`${styles.label} w-1/3`}>Threshold</th>
1426
1427 <th className={`${styles.label} w-1/3`}>Frequency*</th>
1428 </tr>
1429 </thead>
1430 <tbody>
1431 <tr>
1432 <td className="pb-2">
1433 <label
1434 htmlFor="checkboxUntrustedDomains"
1435 className={`${styles.label} flex items-center cursor-pointer`}
1436 >
1437 <input
1438 name="checkboxUntrustedDomains"
1439 id="checkboxUntrustedDomains"
1440 ref={register}
1441 type="checkbox"
1442 className={styles.checkbox}
1443 onClick={() =>
1444 !getValues('checkboxUntrustedDomains') &&
1445 setValue('allAlerts', false)
1446 }
1447 />
1448 <span className="pl-4">Untrusted domains</span>
1449 </label>
1450 </td>
1451 <td className="flex mt-6 pb-2">
1452 <div className="w-5/6">
1453 <Controller
1454 name="sliderUntrustedDomains"
1455 control={control}
1456 defaultValue={0}
1457 render={({ onChange }) =>
1458 <AlertsSlider
1459 marks={setMarks(0, 25, 50)}
1460 max={50}
1461 defaultValue={5}
1462 onChange={onChange}
1463 />
1464 }
1465 />
1466 </div>
1467 <span className={`${styles.sliderValue}`}>
1468 {sliderValue.sliderUntrustedDomains}
1469 </span>
1470 </td>
1471 <td className="pb-2">
1472 <div className="flex">
1473 <div className="flex pr-5">
1474 <input
1475 name="frequencyUntrustedDomains"
1476 ref={register}
1477 id="frequencyUntrustedDomainsDaily"
1478 className={styles.radioBtn}
1479 type="radio"
1480 value="daily"
1481 defaultChecked
1482 />
1483 <label
1484 className={`${styles.radioLabel} mr-4 cursor-pointer`}
1485 htmlFor="frequencyUntrustedDomainsDaily"
1486 >
1487 Daily
1488 </label>
1489 </div>
1490
1491 <div className="flex">
1492 <input
1493 name="frequencyUntrustedDomains"
1494 ref={register}
1495 id="frequencyUntrustedDomainsWeekly"
1496 className={styles.radioBtn}
1497 type="radio"
1498 value="weekly"
1499 />
1500 <label
1501 className={`${styles.radioLabel} cursor-pointer`}
1502 htmlFor="frequencyUntrustedDomainsWeekly"
1503 >
1504 Weekly
1505 </label>
1506 </div>
1507 </div>
1508 </td>
1509 </tr>
1510
1511 <tr>
1512 <td className="pb-2">
1513 <label
1514 htmlFor="checkboxAppDuplications"
1515 className={`${styles.label} flex items-center cursor-pointer`}
1516 >
1517 <input
1518 name="checkboxAppDuplications"
1519 id="checkboxAppDuplications"
1520 ref={register}
1521 type="checkbox"
1522 className={styles.checkbox}
1523 onClick={() =>
1524 !getValues('checkboxAppDuplications') &&
1525 setValue('allAlerts', false)
1526 }
1527 />
1528 <span className="pl-4">App duplications</span>
1529 </label>
1530 </td>
1531 <td className="flex mt-6 pb-2">
1532 <div className="w-5/6">
1533 <Controller
1534 name="sliderAppDuplications"
1535 control={control}
1536 defaultValue={0}
1537 render={({ onChange }) =>
1538 <AlertsSlider
1539 max={100}
1540 defaultValue={10}
1541 onChange={onChange}
1542 />
1543 }
1544 />
1545 </div>
1546 <span className={`${styles.sliderValue}`}>
1547 {sliderValue.sliderAppDuplications} %
1548 </span>
1549 </td>
1550 <td className="pb-2">
1551 <div className="flex">
1552 <div className="flex pr-5">
1553 <input
1554 name="frequencyAppDuplications"
1555 ref={register}
1556 id="frequencyAppDuplicationsDaily"
1557 className={styles.radioBtn}
1558 type="radio"
1559 value="daily"
1560 defaultChecked
1561 />
1562 <label
1563 className={`${styles.radioLabel} mr-4 cursor-pointer`}
1564 htmlFor="frequencyAppDuplicationsDaily"
1565 >
1566 Daily
1567 </label>
1568 </div>
1569
1570 <div className="flex">
1571 <input
1572 name="frequencyAppDuplications"
1573 ref={register}
1574 id="frequencyAppDuplicationsWeekly"
1575 className={styles.radioBtn}
1576 type="radio"
1577 value="weekly"
1578 />
1579 <label
1580 className={`${styles.radioLabel} cursor-pointer`}
1581 htmlFor="frequencyAppDuplicationsWeekly"
1582 >
1583 Weekly
1584 </label>
1585 </div>
1586 </div>
1587 </td>
1588 </tr>
1589
1590 <tr>
1591 <td className="pb-2">
1592 <label
1593 htmlFor="checkboxAdSessionExclusivity"
1594 className={`${styles.label} flex items-center cursor-pointer`}
1595 >
1596 <input
1597 name="checkboxAdSessionExclusivity"
1598 id="checkboxAdSessionExclusivity"
1599 ref={register}
1600 type="checkbox"
1601 className={styles.checkbox}
1602 onClick={() =>
1603 !getValues('checkboxAdSessionExclusivity') &&
1604 setValue('allAlerts', false)
1605 }
1606 />
1607 <span className="pl-4">Ad session exclusivity</span>
1608 </label>
1609 </td>
1610 <td className="flex mt-6 pb-2">
1611 <div className="w-5/6">
1612 <Controller
1613 name="sliderAdSessionExclusivity"
1614 control={control}
1615 defaultValue={0}
1616 render={({ onChange }) =>
1617 <AlertsSlider
1618 max={100}
1619 defaultValue={5}
1620 onChange={onChange}
1621 />
1622 }
1623 />
1624 </div>
1625 <span className={`${styles.sliderValue}`}>
1626 {sliderValue.sliderAdSessionExclusivity} %
1627 </span>
1628 </td>
1629 <td className="pb-2">
1630 <div className="flex">
1631 <div className="flex pr-5">
1632 <input
1633 name="frequencyAdSessionExclusivity"
1634 ref={register}
1635 id="frequencyAdSessionExclusivityDaily"
1636 className={styles.radioBtn}
1637 type="radio"
1638 value="daily"
1639 defaultChecked
1640 />
1641 <label
1642 className={`${styles.radioLabel} mr-4 cursor-pointer`}
1643 htmlFor="frequencyAdSessionExclusivityDaily"
1644 >
1645 Daily
1646 </label>
1647 </div>
1648
1649 <div className="flex">
1650 <input
1651 name="frequencyAdSessionExclusivity"
1652 ref={register}
1653 id="frequencyAdSessionExclusivityWeekly"
1654 className={styles.radioBtn}
1655 type="radio"
1656 value="weekly"
1657 />
1658 <label
1659 className={`${styles.radioLabel} cursor-pointer`}
1660 htmlFor="frequencyAdSessionExclusivityWeekly"
1661 >
1662 Weekly
1663 </label>
1664 </div>
1665 </div>
1666 </td>
1667 </tr>
1668
1669 <tr>
1670 <td className="pb-2">
1671 <label
1672 htmlFor="checkboxBidRisk"
1673 className={`${styles.label} flex items-center cursor-pointer`}
1674 >
1675 <input
1676 name="checkboxBidRisk"
1677 id="checkboxBidRisk"
1678 ref={register}
1679 type="checkbox"
1680 className={styles.checkbox}
1681 onClick={() =>
1682 !getValues('checkboxBidRisk') &&
1683 setValue('allAlerts', false)
1684 }
1685 />
1686 <span className="pl-4">Bid risk</span>
1687 </label>
1688 </td>
1689 <td className="flex mt-6 pb-2">
1690 <div className="w-5/6">
1691 <Controller
1692 name="sliderBidRisk"
1693 control={control}
1694 defaultValue={0}
1695 render={({ onChange }) =>
1696 <AlertsSlider
1697 max={100}
1698 defaultValue={10}
1699 onChange={onChange}
1700 />
1701 }
1702 />
1703 </div>
1704 <span className={`${styles.sliderValue}`}>
1705 {sliderValue.sliderBidRisk} %
1706 </span>
1707 </td>
1708 <td className="pb-2">
1709 <div className="flex">
1710 <div className="flex pr-5">
1711 <input
1712 name="frequencyDidRisk"
1713 ref={register}
1714 id="frequencyDidRiskDaily"
1715 className={styles.radioBtn}
1716 type="radio"
1717 value="daily"
1718 defaultChecked
1719 />
1720 <label
1721 className={`${styles.radioLabel} mr-4 cursor-pointer`}
1722 htmlFor="frequencyDidRiskDaily"
1723 >
1724 Daily
1725 </label>
1726 </div>
1727
1728 <div className="flex">
1729 <input
1730 name="frequencyDidRisk"
1731 ref={register}
1732 id="frequencyDidRiskWeekly"
1733 className={styles.radioBtn}
1734 type="radio"
1735 value="weekly"
1736 />
1737 <label
1738 className={`${styles.radioLabel} cursor-pointer`}
1739 htmlFor="frequencyDidRiskWeekly"
1740 >
1741 Weekly
1742 </label>
1743 </div>
1744 </div>
1745 </td>
1746 </tr>
1747
1748 <tr>
1749 <td className="pb-2">
1750 <label
1751 htmlFor="checkboxClickProxies"
1752 className={`${styles.label} flex items-center cursor-pointer`}
1753 >
1754 <input
1755 name="checkboxClickProxies"
1756 id="checkboxClickProxies"
1757 ref={register}
1758 type="checkbox"
1759 className={styles.checkbox}
1760 onClick={() =>
1761 !getValues('checkboxClickProxies') &&
1762 setValue('allAlerts', false)
1763 }
1764 />
1765 <span className="pl-4 -mt-1">Click proxies</span>
1766 </label>
1767 </td>
1768 <td className="flex mt-6 pb-2">
1769 <div className="w-5/6">
1770 <Controller
1771 name="sliderClickProxies"
1772 control={control}
1773 defaultValue={0}
1774 render={({ onChange }) =>
1775 <AlertsSlider
1776 marks={setMarks(0, 25, 50)}
1777 max={50}
1778 defaultValue={2}
1779 onChange={onChange}
1780 />
1781 }
1782 />
1783 </div>
1784 <span className={`${styles.sliderValue}`}>
1785 {sliderValue.sliderClickProxies}
1786 </span>
1787 </td>
1788 <td className="pb-2">
1789 <div className="flex">
1790 <div className="flex pr-5">
1791 <input
1792 name="frequencyClickProxies"
1793 ref={register}
1794 id="frequencyClickProxiesDaily"
1795 className={styles.radioBtn}
1796 type="radio"
1797 value="daily"
1798 defaultChecked
1799 />
1800 <label
1801 className={`${styles.radioLabel} mr-4 cursor-pointer`}
1802 htmlFor="frequencyClickProxiesDaily"
1803 >
1804 Daily
1805 </label>
1806 </div>
1807
1808 <div className="flex">
1809 <input
1810 name="frequencyClickProxies"
1811 ref={register}
1812 id="frequencyClickProxiesWeekly"
1813 className={styles.radioBtn}
1814 type="radio"
1815 value="weekly"
1816 />
1817 <label
1818 className={`${styles.radioLabel} cursor-pointer`}
1819 htmlFor="frequencyClickProxiesWeekly"
1820 >
1821 Weekly
1822 </label>
1823 </div>
1824 </div>
1825 </td>
1826 </tr>
1827 </tbody>
1828 </table>
1829 </div>
1830 );
1831};
1832
1833export default AlertsThreshold;
1834import PlusIconGray from '../../icons/PlusIconGray';
1835import BinIcon from '../../icons/BinIcon';
1836
1837import './styles.css';
1838
1839const styles = {
1840 tableWrapper: `align-middle inline-block min-w-full overflow-hidden border-b border-gray-200 bg-table border border-table-border`,
1841 table: `w-full min-w-full divide-y divide-gray-200 table-fixed`,
1842
1843 tableTheadTr: `border-b border-table-border text-table-list-font text-left text-12px font-body tracking-wider select-none`,
1844 tableTh: `w-1/3 px-6 py-3 whitespace-no-wrap border truncate`,
1845
1846 tableTbodyTr: `whitespace-no-wrap text-13px text-custom-blue font-body hover:shadow-md hover:bg-white`,
1847
1848 creative__btn_group: `flex cell-icon opacity-0 transition-opacity duration-200`,
1849
1850 creative__btn_plus: `ml-5 h-6 flex items-center px-2 bg-custom-blue bg-opacity-8 rounded cursor-pointer`,
1851 creative__btn_plus_text: `pl-2 font-body text-custom-blue text-opacity-50`,
1852
1853 creative__btn_delete: `ml-5 h-6 flex items-center px-2 bg-custom-blue bg-opacity-8 rounded cursor-pointer`,
1854 creative__btn_delete_text: `pl-2 font-body text-custom-blue text-opacity-50`,
1855
1856 dsp__cell: `flex items-center justify-between`,
1857 dsp__btn_plus: `ml-5 h-6 flex items-center px-2 bg-custom-blue bg-opacity-8 rounded cell-icon opacity-0 transition-opacity duration-200 cursor-pointer`,
1858 dsp__btn_plus_text: `pl-2 font-body text-custom-blue text-opacity-50`,
1859};
1860
1861const CreateCampaignTable = ({
1862 onAddDSPClick,
1863 onDeleteCreativeClick,
1864 onAddSSPClick,
1865 tableData,
1866}) => {
1867 return (
1868 <div className={styles.tableWrapper}>
1869 <table className={styles.table}>
1870 <thead>
1871 <tr className={styles.tableTheadTr}>
1872 <th className={styles.tableTh}>Creative name</th>
1873 <th className={styles.tableTh}>DSP</th>
1874 <th className={styles.tableTh}>SSP</th>
1875 </tr>
1876 </thead>
1877
1878 <tbody className="align-top">
1879 {tableData.map((creative, index) => {
1880 return (
1881 <tr
1882 key={index}
1883 className={`${styles.tableTbodyTr} ${
1884 creative.name && 'border-t border-table-border'
1885 }`}
1886 >
1887 <td className="px-6 py-3 align-middle">
1888 <div className="h-full flex items-center">
1889 <p>{creative.name}</p>
1890 {creative.name && (
1891 <div className={styles.creative__btn_group}>
1892 <div
1893 className={styles.creative__btn_plus}
1894 onClick={() => onAddDSPClick(creative)}
1895 >
1896 <PlusIconGray />
1897 <span className={styles.creative__btn_plus_text}>
1898 Add DSP
1899 </span>
1900 </div>
1901 {!creative.id && (
1902 <div
1903 className={styles.creative__btn_delete}
1904 onClick={() => onDeleteCreativeClick(creative)}
1905 >
1906 <BinIcon />
1907 <span className={styles.creative__btn_delete_text}>
1908 Delete Creative
1909 </span>
1910 </div>
1911 )}
1912 </div>
1913 )}
1914 </div>
1915 </td>
1916
1917 <td className="px-6 py-3 border-l">
1918 {creative.dsps.map((dsp, dspIndex) => {
1919 return (
1920 <div
1921 key={dspIndex}
1922 className={`${styles.dsp__cell} pb-3`}
1923 >
1924 <p>{dsp.name}</p>
1925 {dsp.name && (
1926 <div
1927 className={styles.dsp__btn_plus}
1928 onClick={() => onAddSSPClick(creative, dsp)}
1929 >
1930 <PlusIconGray />
1931 <span className={styles.dsp__btn_plus_text}>
1932 Add SSP
1933 </span>
1934 </div>
1935 )}
1936 </div>
1937 );
1938 })}
1939 </td>
1940
1941 <td className="px-6 py-3 border-l">
1942 {creative.dsps.map((dsp, dspIndex) => {
1943 const ssps = dsp.ssps
1944 .map(ssp => {
1945 return ssp.name;
1946 })
1947 .join(', ');
1948 return (
1949 <div
1950 key={dspIndex}
1951 className={`${styles.dsp__cell} pb-3`}
1952 >
1953 <p className="truncate">{ssps}</p>
1954 <div className="ml-5 h-6 flex" />
1955 </div>
1956 );
1957 })}
1958 </td>
1959 </tr>
1960 );
1961 })}
1962 </tbody>
1963 </table>
1964 </div>
1965 );
1966};
1967
1968export default CreateCampaignTable;
1969import Modal from 'react-modal';
1970
1971import CrossIcon from '../../icons/CrossIcon';
1972import WarningPopUpIcon from '../../icons/WarningPopUpIcon';
1973
1974const styles = {
1975 iconWrapper: `h-20 w-20 bg-gray-100 flex items-center justify-center rounded-full text-center`,
1976 title: `w-330px text-center font-body text-20px text-custom-blue`,
1977 btn: `w-32 py-2 rounded-lg font-body text-14px select-none`,
1978};
1979
1980const modalStyles = {
1981 content: {
1982 height: 'auto',
1983 width: '430px',
1984 border: 'none',
1985 boxShadow: '0px 0px 10px 0px lightgray',
1986 top: '50%',
1987 left: '50%',
1988 right: 'auto',
1989 bottom: 'auto',
1990 marginRight: '-50%',
1991 transform: 'translate(-50%, -50%)',
1992 },
1993};
1994
1995const ErrorPopUp = ({ isOpen, onClose }) => {
1996 return (
1997 <Modal isOpen={isOpen} style={modalStyles}>
1998 <div
1999 className="flex justify-end cursor-pointer"
2000 onClick={() => onClose()}
2001 >
2002 <CrossIcon />
2003 </div>
2004
2005 <div className="flex justify-center mb-6">
2006 <div className={styles.iconWrapper}>
2007 <div>
2008 <WarningPopUpIcon />
2009 </div>
2010 </div>
2011 </div>
2012
2013 <div className="flex justify-center mb-6">
2014 <p className={styles.title}>
2015 An error occurred please try again later
2016 </p>
2017 </div>
2018
2019 <div className="flex justify-center mb-6">
2020 <button
2021 type="button"
2022 className={`${styles.btn} bg-custom-red text-white`}
2023 >
2024 <span className="px-8">Retry</span>
2025 </button>
2026 </div>
2027 </Modal>
2028 );
2029};
2030
2031export default ErrorPopUp;
2032import get from 'lodash/get';
2033
2034export const formatAdvertizersResponse = data => {
2035 const formattedData = (get(data, 'advertizers.edges') || []).map(edge => {
2036 const { node } = edge;
2037 const { name, id } = node;
2038 return {
2039 id,
2040 name,
2041 };
2042 });
2043
2044 return formattedData;
2045};
2046
2047export const formatDspsResponse = data => {
2048 const formattedData = (get(data, 'dsps.edges') || []).map(edge => {
2049 const { node } = edge;
2050 const { name, id } = node;
2051 return {
2052 id,
2053 name,
2054 };
2055 });
2056
2057 return formattedData;
2058};
2059
2060export const formatSspsResponse = data => {
2061 const formattedData = (get(data, 'ssps.edges') || []).map(edge => {
2062 const { node } = edge;
2063 const { name, id } = node;
2064 return {
2065 id,
2066 name,
2067 };
2068 });
2069
2070 return formattedData;
2071};
2072import { useState, useEffect } from 'react';
2073import Select from 'react-select';
2074import CreatableSelect from 'react-select/creatable';
2075import { Link } from 'react-router-dom';
2076import { useForm, Controller, FormProvider } from 'react-hook-form';
2077import { useParams } from 'react-router';
2078
2079import PlusIcon from '../../icons/PlusIcon';
2080import DownloadIcon from '../../icons/DownloadIcon';
2081import BackArrowIcon from '../../icons/BackArrowIcon';
2082
2083import CreateCampaignTable from './CreateCampaignTable';
2084import AlertsThreshold from './AlertsThreshold';
2085import PopUp from './PopUp';
2086import PopUpSuccess from './PopUpSuccess';
2087import PopUpChoice from './PopUpChoice';
2088
2089// import ErrorPopUp from './ErrorPopUp';
2090
2091import { MultipleSelectStyles, selectStyles } from '../selectStyle';
2092
2093import { useGetAdvertizersQuery, useGetCampaignQuery } from './queries';
2094import { formatAdvertizersResponse } from './helpers';
2095import { formatCampaignResponse } from '../campaignsList/helpers';
2096
2097const styles = {
2098 nav: `flex h-16 border-b border-gray-400 items-center`,
2099 navTitle: `text-3xl text-custom-blue px-5 py-2 font-bold font-body select-none`,
2100
2101 title: `pb-3 text-table-list-font font-body text-14px font-bold`,
2102 description: `pb-3 text-table-list-font font-body text-13px`,
2103
2104 Btn: `py-2 px-4 rounded-lg font-body text-14px select-none`,
2105 searchField: `border rounded-lg py-2 px-3 placeholder-custom-blue font-body text-14px leading-tight focus:outline-none select-none`,
2106
2107 errorMessage: `text-12px text-custom-red mt-2 font-body leading-3`,
2108};
2109
2110const CreateCampaignPage = () => {
2111 const methods = useForm({
2112 defaultValues: {
2113 allAlerts: true,
2114 checkboxUntrustedDomains: true,
2115 checkboxAppDuplications: true,
2116 checkboxAdSessionExclusivity: true,
2117 checkboxBidRisk: true,
2118 checkboxClickProxies: true,
2119 sliderUntrustedDomains: 5,
2120 sliderAppDuplications: 10,
2121 sliderAdSessionExclusivity: 5,
2122 sliderBidRisk: 10,
2123 sliderClickProxies: 2,
2124 emails: [],
2125 },
2126 });
2127 const {
2128 register,
2129 control,
2130 handleSubmit,
2131 setValue,
2132 getValues,
2133 errors,
2134 reset,
2135 } = methods;
2136 const onSubmit = data => {
2137 console.log(data);
2138
2139 !isTableEmpty && setIsSuccessModalOpen(true);
2140 };
2141
2142 const [isModalOpen, setIsModalOpen] = useState(false);
2143 const [modalType, setModalType] = useState('CREATIVE');
2144
2145 const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
2146
2147 const [isChoiceModalOpen, setIsChoiceModalOpen] = useState(false);
2148 const [choiceModalType, setChoiceModalType] = useState('CANCEL');
2149
2150 const { data: advertizersData } = useGetAdvertizersQuery();
2151
2152 const [tableData, setTableData] = useState([]);
2153 const [currentCreative, setCurrentCreative] = useState(null);
2154 const [currentDsp, setCurrentDsp] = useState(null);
2155
2156 const { id } = useParams();
2157 const isEditMode = !!id;
2158 const { data, loading } = useGetCampaignQuery({ variables: { id } });
2159
2160 const [inputValue, setInputValue] = useState('');
2161 const [isEmailValid, setIsEmailValid] = useState(true);
2162 const [isTableEmpty, setIsTableEmpty] = useState(false);
2163
2164 const campaign = formatCampaignResponse(data);
2165 useEffect(() => {
2166 if (data && data.campaign) {
2167 reset({
2168 campaignName: campaign.name,
2169 advertiser: {
2170 value: campaign.advertizer.id,
2171 label: campaign.advertizer.name,
2172 },
2173 allAlerts: true,
2174 checkboxUntrustedDomains: true,
2175 checkboxAppDuplications: true,
2176 checkboxAdSessionExclusivity: true,
2177 checkboxBidRisk: true,
2178 checkboxClickProxies: true,
2179 emails: [],
2180 });
2181 setTableData(campaign.creatives);
2182 }
2183 }, [data]); // eslint-disable-line react-hooks/exhaustive-deps
2184
2185 if (loading) {
2186 return <div>Loading</div>;
2187 }
2188
2189 const advertizers = formatAdvertizersResponse(advertizersData);
2190 const advertizersOptions = advertizers.map(advertizer => {
2191 return {
2192 value: advertizer.id,
2193 label: advertizer.name,
2194 };
2195 });
2196
2197 const setTypeAndOpenModal = type => {
2198 setModalType(type);
2199 setIsModalOpen(true);
2200 };
2201
2202 const createOption = label => ({
2203 label,
2204 value: label,
2205 });
2206
2207 const handleKeyDown = event => {
2208 if (!inputValue) return;
2209
2210 const regEmailValidator = /^\w+([-]?\w+)*@\w+([-]?\w+)*(\.\w{2,3})+$/;
2211
2212 switch (event.which) {
2213 case 13:
2214 case 32:
2215 case 9:
2216 if (regEmailValidator.test(inputValue)) {
2217 setIsEmailValid(true);
2218 setInputValue('');
2219 setValue(
2220 'emails',
2221 [...getValues('emails'), createOption(inputValue)],
2222 { shouldValidate: true }
2223 );
2224 } else {
2225 setIsEmailValid(false);
2226 }
2227
2228 event.preventDefault();
2229 break;
2230 default:
2231 return;
2232 }
2233 };
2234
2235 const updateTableData = (data, nextType) => {
2236 if (modalType === 'CREATIVE') {
2237 const dsp = {
2238 name: data.dsp.label,
2239 id: data.dsp.value,
2240 ssps: data.ssps.map(ssp => ({
2241 name: ssp.label,
2242 id: ssp.value,
2243 })),
2244 };
2245 const newCreativeEntry = {
2246 budget: data.budget,
2247 cpm: data.cpm,
2248 name: data.creativeName,
2249 dsps: [dsp],
2250 };
2251 setTableData([...tableData, newCreativeEntry]);
2252 if (nextType === 'DSP') {
2253 setCurrentCreative(newCreativeEntry);
2254 }
2255 } else if (modalType === 'DSP') {
2256 const dsp = {
2257 name: data.dsp.label,
2258 id: data.dsp.value,
2259 ssps: data.ssps.map(ssp => ({
2260 name: ssp.label,
2261 id: ssp.value,
2262 })),
2263 };
2264 const creative = tableData.find(
2265 creative => creative.name === currentCreative.name
2266 );
2267 creative.dsps.push(dsp);
2268 setTableData([...tableData]);
2269 if (nextType === 'SSP') {
2270 setCurrentDsp(dsp);
2271 }
2272 } else if (modalType === 'SSP') {
2273 const creative = tableData.find(
2274 creative => creative.name === currentCreative.name
2275 );
2276 const dsp = creative.dsps.find(dsp => dsp.name === currentDsp.name);
2277 const newEntries = data.ssps.map(ssp => {
2278 const presentSspEntry = dsp.ssps.find(
2279 sspEntry => sspEntry.id === ssp.value
2280 );
2281 if (presentSspEntry) {
2282 return presentSspEntry;
2283 }
2284 return {
2285 name: ssp.label,
2286 id: ssp.value,
2287 };
2288 });
2289 dsp.ssps = newEntries;
2290 setTableData([...tableData]);
2291 }
2292
2293 if (nextType) {
2294 setTypeAndOpenModal(nextType);
2295 } else {
2296 setCurrentCreative(null);
2297 setCurrentDsp(null);
2298 setIsModalOpen(false);
2299 }
2300 };
2301
2302 const setNewTableData = () => {
2303 const newTableData = tableData.filter(
2304 item => item.name !== currentCreative.name
2305 );
2306 setTableData(newTableData);
2307 setCurrentCreative(null);
2308 };
2309
2310 const updateTableValidationState = () => setIsTableEmpty(false);
2311
2312 return (
2313 <FormProvider {...methods}>
2314 <div>
2315 {isModalOpen && (
2316 <PopUp
2317 type={modalType}
2318 isOpen={isModalOpen}
2319 onClose={() => {
2320 setIsModalOpen(false);
2321 setCurrentCreative(null);
2322 setCurrentDsp(null);
2323 }}
2324 updateTableData={updateTableData}
2325 updateTableValidationState={updateTableValidationState}
2326 creative={currentCreative}
2327 dsp={currentDsp}
2328 tableData={tableData}
2329 isEditMode={isEditMode}
2330 />
2331 )}
2332
2333 {/* <ErrorPopUp isOpen /> */}
2334
2335 <PopUpSuccess
2336 isOpen={isSuccessModalOpen}
2337 onClose={() => setIsSuccessModalOpen(false)}
2338 />
2339
2340 <PopUpChoice
2341 type={choiceModalType}
2342 tableData={tableData}
2343 setNewTableData={() => setNewTableData()}
2344 isOpen={isChoiceModalOpen}
2345 onClose={() => setIsChoiceModalOpen(false)}
2346 />
2347 <form onSubmit={handleSubmit(onSubmit)}>
2348 <div className="flex flex-col h-full">
2349 <nav className={styles.nav}>
2350 <div className="pl-5">
2351 <BackArrowIcon />
2352 </div>
2353 <Link
2354 to="/campaigns"
2355 className="pl-1 py-5 text-nav-link font-body"
2356 >
2357 Campaigns
2358 </Link>
2359 <p className={styles.navTitle}>
2360 {isEditMode ? 'Edit' : 'Create'} campaign
2361 </p>
2362 </nav>
2363
2364 <div className="bg-main p-12 overflow-x-auto flex-grow">
2365 <div className="pb-6">
2366 <p className={styles.title}>ADVERTISER</p>
2367 <p className={styles.description}>
2368 Select the advertiser of the campaign
2369 </p>
2370
2371 <Controller
2372 name="advertiser"
2373 control={control}
2374 rules={{ required: true }}
2375 as={
2376 <Select
2377 theme={theme => ({
2378 ...theme,
2379 colors: { primary25: '#f5f4f4' },
2380 })}
2381 defaultValue=""
2382 styles={selectStyles}
2383 options={advertizersOptions}
2384 components={{
2385 IndicatorSeparator: null,
2386 DropdownIndicator: null,
2387 }}
2388 placeholder="Search..."
2389 isDisabled={isEditMode}
2390 />
2391 }
2392 />
2393 {errors.advertiser && (
2394 <p className={styles.errorMessage}>Field is required</p>
2395 )}
2396 </div>
2397
2398 <div className="pb-6">
2399 <p className={styles.title}>CAMPAIGN NAME</p>
2400 <p className={styles.description}>
2401 Enter the name of the new campaign. An advertiser campaign
2402 name must be unique
2403 </p>
2404 <div className="relative my-auto">
2405 <input
2406 name="campaignName"
2407 ref={register({ required: true })}
2408 className={`${styles.searchField} w-64 pl-3 ${
2409 isEditMode &&
2410 'bg-disabled-input text-custom-blue text-opacity-60'
2411 }`}
2412 type="text"
2413 placeholder="Campaign name"
2414 disabled={isEditMode}
2415 />
2416 {errors.campaignName && (
2417 <p className={styles.errorMessage}>Field is required</p>
2418 )}
2419 </div>
2420 </div>
2421
2422 <div className="flex justify-between">
2423 <div>
2424 <p className={styles.title}>CREATIVE NAME</p>
2425 <p className={styles.description}>
2426 Add or edit creative grouping
2427 </p>
2428 </div>
2429
2430 <button
2431 type="button"
2432 className={`${styles.Btn} bg-custom-red text-white mb-6`}
2433 onClick={() => setTypeAndOpenModal('CREATIVE')}
2434 >
2435 <PlusIcon />
2436 <span className="pl-2">Add creative</span>
2437 </button>
2438 </div>
2439
2440 <div className="pb-6">
2441
2442 {
2443 tableData.length
2444 ? <CreateCampaignTable
2445 onAddDSPClick={creative => {
2446 setTypeAndOpenModal('DSP');
2447 setCurrentCreative(creative);
2448 }}
2449 onAddSSPClick={(creative, dsp) => {
2450 setTypeAndOpenModal('SSP');
2451 setCurrentCreative(creative);
2452 setCurrentDsp(dsp);
2453 }}
2454 onDeleteCreativeClick={creative => {
2455 setChoiceModalType('DELETE');
2456 setIsChoiceModalOpen(true);
2457 setCurrentCreative(creative);
2458 }}
2459 tableData={tableData}
2460 />
2461 : <div className="border border-table-border bg-table">
2462 <p className="p-4 font-body text-table-list-font text-13px">
2463 No creatives created
2464 </p>
2465 </div>
2466 }
2467
2468 {
2469 isTableEmpty &&
2470 <p className={styles.errorMessage}>
2471 Please add at least 1 creative
2472 </p>
2473 }
2474
2475 </div>
2476
2477 <div className="pb-6">
2478 <p className={styles.title}>ALERTS THRESHOLD</p>
2479 <p className={styles.description}>
2480 dedupe sends emails during the campaign. Uncheck any alert as
2481 required
2482 </p>
2483
2484 <AlertsThreshold />
2485 </div>
2486
2487 <div className="pb-6">
2488 <p className={styles.description}>
2489 *dedupe automatically sends an alerts 6 hours after the first
2490 impression is recorded
2491 </p>
2492 </div>
2493
2494 <div className="pb-6">
2495 <div>
2496 <p className={styles.title}>EMAIL DISTRIBUTION</p>
2497 <p className={styles.description}>Enter emails...</p>
2498
2499 <Controller
2500 name="emails"
2501 control={control}
2502 defaultValue={[]}
2503 as={
2504 <CreatableSelect
2505 styles={MultipleSelectStyles}
2506 components={{ DropdownIndicator: null }}
2507 isClearable
2508 isMulti
2509 menuIsOpen={false}
2510 onKeyDown={handleKeyDown}
2511 placeholder="Enter email here..."
2512 inputValue={inputValue}
2513 onChange={value => setValue('emails', value)}
2514 onInputChange={inputValue => setInputValue(inputValue)}
2515 value={getValues('emails')}
2516 defaultValue={[]}
2517 />
2518 }
2519 rules={{
2520 validate: value => {
2521 return !!value.length;
2522 },
2523 }}
2524 />
2525 {errors.emails && (
2526 <p className={styles.errorMessage}>Field is required</p>
2527 )}
2528 {!isEmailValid && (
2529 <p className={styles.errorMessage}>
2530 Email must contain @ and domain
2531 </p>
2532 )}
2533 </div>
2534 </div>
2535
2536 <div className="flex pb-5">
2537 <button
2538 type="button"
2539 className={`${styles.Btn} bg-black bg-opacity-5 text-custom-blue`}
2540 onClick={() => {
2541 setChoiceModalType('CANCEL');
2542 setIsChoiceModalOpen(true);
2543 }}
2544 >
2545 Cancel creation
2546 </button>
2547 <button
2548 type="submit"
2549 className={`${styles.Btn} bg-custom-red text-white ml-3`}
2550 onClick={() =>
2551 !tableData.length
2552 ? setIsTableEmpty(true)
2553 : setIsTableEmpty(false)
2554 }
2555 >
2556 <DownloadIcon />
2557 <span className="pl-2">Save and Download tags</span>
2558 </button>
2559 </div>
2560 </div>
2561 </div>
2562 </form>
2563 </div>
2564 </FormProvider>
2565 );
2566};
2567
2568export default CreateCampaignPage;
2569import CrossIcon from '../../icons/CrossIcon';
2570import * as SelectModule from 'react-select';
2571import Select from 'react-select';
2572import DollarIcon from '../../icons/DollarIcon';
2573import Modal from 'react-modal';
2574import { useGetDspsQuery, useGetSspsQuery } from './queries';
2575import { formatDspsResponse, formatSspsResponse } from './helpers';
2576import { useForm, Controller } from 'react-hook-form';
2577
2578const fromSpToSelectOption = sp => {
2579 return {
2580 value: sp.id,
2581 label: sp.name,
2582 };
2583};
2584
2585const PopUp = props => {
2586 const formData = useForm({
2587 defaultValues: {
2588 creativeName: props.creative ? props.creative.name : '',
2589 dsp: props.dsp
2590 ? fromSpToSelectOption(props.dsp)
2591 : {
2592 label: (
2593 <span className="text-custom-blue text-opacity-60">
2594 Search DSP...
2595 </span>
2596 ),
2597 value: undefined,
2598 },
2599 ssps: props.dsp ? props.dsp.ssps.map(fromSpToSelectOption) : [],
2600 cpm: null,
2601 budget: null,
2602 domain: '',
2603 },
2604 });
2605
2606 const { register, control, handleSubmit, setValue, errors, watch } = formData;
2607 const sspsInInput = watch('ssps');
2608
2609 const { data: dspsData } = useGetDspsQuery();
2610 const { data: sspsData } = useGetSspsQuery();
2611 const dsps = formatDspsResponse(dspsData);
2612 const dspsOptions = dsps.map(fromSpToSelectOption);
2613
2614 const ssps = formatSspsResponse(sspsData);
2615 const sspsOptions = ssps.map(fromSpToSelectOption);
2616
2617 const modalStyles = {
2618 content: {
2619 height: 'auto',
2620 width: '430px',
2621 border: 'none',
2622 boxShadow: '0px 0px 10px 0px lightgray',
2623 top: '50%',
2624 left: '50%',
2625 right: 'auto',
2626 bottom: 'auto',
2627 marginRight: '-50%',
2628 transform: 'translate(-50%, -50%)',
2629 },
2630 };
2631
2632 Modal.setAppElement('#root');
2633
2634 const styles = {
2635 searchField: `border border-table-border rounded-md py-2 px-3 placeholder-custom-blue text-custom-blue placeholder-opacity-60 font-body leading-tight focus:outline-none select-none`,
2636 Btn: `py-2 px-4 rounded-lg font-body text-14px select-none`,
2637
2638 errorMessage: `text-12px text-custom-red mt-2 font-body leading-3`,
2639 };
2640
2641 const onSubmit = (data, e) => {
2642 const { nextType } = e.target.dataset;
2643
2644 if (nextType === 'DSP') {
2645 setValue('dsp', null);
2646 setValue('ssps', []);
2647 }
2648 props.updateTableData(data, nextType);
2649 props.updateTableValidationState();
2650 };
2651
2652 const sspsFromServer = props.dsp
2653 ? props.dsp.ssps.filter(ssp => ssp.fromServer)
2654 : [];
2655 const hasSspFromServer = sspsFromServer.length > 0;
2656 const hasSspFromClient = sspsInInput > sspsFromServer;
2657
2658 return (
2659 <Modal
2660 isOpen={props.isOpen}
2661 style={modalStyles}
2662 contentLabel="Example Modal"
2663 >
2664 <form onSubmit={handleSubmit(onSubmit)}>
2665 <div className="flex pb-5">
2666 <p className="font-body text-custom-blue text-20px font-bold">
2667 {props.type === 'CREATIVE' && `Create creative`}
2668 {props.type === 'DSP' && `Add DSP`}
2669 {props.type === 'SSP' && `Add SSP`}
2670 </p>
2671 <div
2672 className="ml-auto cursor-pointer"
2673 onClick={() => props.onClose()}
2674 >
2675 <CrossIcon />
2676 </div>
2677 </div>
2678
2679 <div className="pb-5">
2680 <p className="font-body text-table-list-font text-14px pb-3 font-bold">
2681 {props.type === 'CREATIVE' && `ENTER`} CREATIVE GROUPING NAME
2682 </p>
2683
2684 <input
2685 name="creativeName"
2686 ref={register({
2687 required: 'Field is required',
2688 validate: {
2689 uniqueName: value => {
2690 if (props.type !== 'CREATIVE') {
2691 return true;
2692 }
2693 const isSameCreativeNameExist = props.tableData.some(
2694 creative => {
2695 return creative.name === value;
2696 }
2697 );
2698 return (
2699 !isSameCreativeNameExist ||
2700 'Creative grouping with this name already exists, please provide another name'
2701 );
2702 },
2703 },
2704 })}
2705 className={`${styles.searchField} w-full text-14px ${
2706 (props.type === 'SSP' || props.type === 'DSP') &&
2707 'text-opacity-60'
2708 }
2709 `}
2710 placeholder={
2711 props.creative && (props.type === 'DSP' || props.type === 'SSP')
2712 ? props.creative.name
2713 : 'Enter creative name...'
2714 }
2715 disabled={props.type === 'DSP' || props.type === 'SSP'}
2716 defaultValue={props.creative ? props.creative.name : ''}
2717 />
2718 {errors.creativeName && (
2719 <p className={styles.errorMessage}>
2720 {errors.creativeName.message}
2721 </p>
2722 )}
2723 </div>
2724
2725 <div className="pb-5">
2726 <p className="font-body text-table-list-font text-14px pb-3 font-bold">
2727 DSP
2728 </p>
2729
2730 <p className="font-body text-table-list-font text-13px pb-3">
2731 {props.type !== 'SSP' && `Select the DSP or Ad Network`}
2732 </p>
2733
2734 <div className="relative my-auto">
2735 <Controller
2736 name="dsp"
2737 control={control}
2738 as={
2739 <Select
2740 isClearable
2741 isSearchable
2742 components={{
2743 IndicatorSeparator: null,
2744 DropdownIndicator: null,
2745 ClearIndicator: null,
2746 }}
2747 styles={
2748 props.type === 'SSP' && {
2749 control: () => ({
2750 background: '#fafafa',
2751 borderRadius: 6,
2752 border: '1px solid #e5e5e5',
2753 }),
2754 }
2755 }
2756 options={dspsOptions}
2757 className="font-body text-14px"
2758 isDisabled={props.type === 'SSP'}
2759 defaultValue={
2760 props.type === 'SSP' &&
2761 fromSpToSelectOption(props.dsp || {})
2762 }
2763 />
2764 }
2765 rules={{
2766 validate: value => {
2767 console.log(value);
2768 return value.value !== undefined;
2769 },
2770 }}
2771 />
2772 {errors.dsp && (
2773 <p className={styles.errorMessage}>
2774 DSP is required
2775 </p>
2776 )}
2777 </div>
2778 </div>
2779
2780 <div className="pb-5">
2781 <p className="font-body text-table-list-font text-14px pb-3 font-bold">
2782 SSP
2783 </p>
2784
2785 <p className="font-body text-table-list-font text-13px pb-3">
2786 {props.type === 'SSP'
2787 ? 'Select at least 1 SSP'
2788 : 'Select at least 2 SSPs'}
2789 </p>
2790
2791 <Controller
2792 name="ssps"
2793 control={control}
2794 as={
2795 <Select
2796 isMulti
2797 components={{
2798 IndicatorSeparator: null,
2799 DropdownIndicator: null,
2800 ClearIndicator: selectProps => {
2801 if (
2802 !props.isEditMode ||
2803 props.type === 'CREATIVE' ||
2804 props.type === 'DSP'
2805 ) {
2806 return (
2807 <SelectModule.components.ClearIndicator
2808 {...selectProps}
2809 />
2810 );
2811 }
2812 if (!hasSspFromServer) {
2813 return (
2814 <SelectModule.components.ClearIndicator
2815 {...selectProps}
2816 />
2817 );
2818 }
2819
2820 return null;
2821 },
2822 MultiValueRemove: selectProps => {
2823 if (
2824 !props.isEditMode ||
2825 props.type === 'CREATIVE' ||
2826 props.type === 'DSP'
2827 ) {
2828 return (
2829 <SelectModule.components.MultiValueRemove
2830 {...selectProps}
2831 />
2832 );
2833 }
2834 const ssp = sspsFromServer.find(
2835 ssp => ssp.id === selectProps.data.value
2836 );
2837
2838 if (!ssp) {
2839 return (
2840 <SelectModule.components.MultiValueRemove
2841 {...selectProps}
2842 />
2843 );
2844 }
2845
2846 return null;
2847 },
2848 }}
2849 backspaceRemovesValue={hasSspFromClient}
2850 clearable={!props.isEditMode || !hasSspFromServer}
2851 options={sspsOptions}
2852 className="basic-multi-select font-body text-14px"
2853 classNamePrefix="select"
2854 placeholder="Enter SSPs here..."
2855 defaultValue={
2856 props.dsp ? props.dsp.ssps.map(fromSpToSelectOption) : null
2857 }
2858 />
2859 }
2860 rules={{
2861 validate: value => {
2862 return value.length >= 2;
2863 },
2864 }}
2865 />
2866 {errors.ssps && (
2867 <p className={styles.errorMessage}>
2868 Select at least 2 SSPs
2869 </p>
2870 )}
2871 </div>
2872
2873 {props.type === 'CREATIVE' && (
2874 <>
2875 <div className="pb-5">
2876 <p className="font-body text-table-list-font text-14px pb-3 font-bold">
2877 TARGET CPM AND BUDGET
2878 </p>
2879
2880 <div className="flex">
2881 <div className="relative my-auto pr-3">
2882 <p className="font-body text-table-list-font text-13px pb-3">
2883 Enter target CPM
2884 </p>
2885 <div className="absolute pl-3 pt-3">
2886 <DollarIcon />
2887 </div>
2888 <input
2889 name="cpm"
2890 ref={register({
2891 pattern: {
2892 value: /^\$?[0-9]+(\.[0-9][0-9])?$/,
2893 message: 'Invalid money format',
2894 },
2895 })}
2896 className={`${styles.searchField} w-full pl-10 font-body text-14px`}
2897 placeholder="0.00"
2898 />
2899 {errors.cpm && (
2900 <p className={styles.errorMessage}>
2901 {errors.cpm.message}
2902 </p>
2903 )}
2904 </div>
2905
2906 <div className="relative my-auto pl-3">
2907 <p className="font-body text-table-list-font text-13px pb-3">
2908 Enter budget
2909 </p>
2910 <div className="absolute pl-3 pt-3">
2911 <DollarIcon />
2912 </div>
2913 <input
2914 name="budget"
2915 ref={register({
2916 pattern: {
2917 value: /^\$?[0-9]+(\.[0-9][0-9])?$/,
2918 message: 'Invalid money format',
2919 },
2920 })}
2921 className={`${styles.searchField} w-full pl-10 font-body text-14px`}
2922 placeholder="0.00"
2923 />
2924 {errors.budget && (
2925 <p className={styles.errorMessage}>
2926 {errors.budget.message}
2927 </p>
2928 )}
2929 </div>
2930 </div>
2931 </div>
2932
2933 <div className="pb-5">
2934 <p className="font-body text-table-list-font text-14px pb-3 font-bold">
2935 CLICK URL DOMAIN
2936 </p>
2937
2938 <p className="font-body text-table-list-font text-13px pb-3">
2939 Enter URL Domain
2940 </p>
2941
2942 <input
2943 name="domain"
2944 ref={register({
2945 required: 'Field is required',
2946 pattern: {
2947 value: /^((https?|ftp|smtp):\/\/)?(www.)?[a-z0-9]+\.[a-z]+(\/[a-zA-Z0-9#]+\/?)*$/,
2948 message: 'Invalid URL format',
2949 },
2950 })}
2951 className={`${styles.searchField} w-full font-body text-14px`}
2952 placeholder="URL Domain"
2953 />
2954 {errors.domain && (
2955 <p className={styles.errorMessage}>
2956 {errors.domain.message}
2957 </p>
2958 )}
2959 </div>
2960 </>
2961 )}
2962
2963 <div
2964 className={`flex ${
2965 props.type === 'SSP' ? 'justify-end' : 'justify-between'
2966 }`}
2967 >
2968 {props.type !== 'SSP' && (
2969 <button
2970 type="button"
2971 data-next-type={'DSP'}
2972 onClick={handleSubmit(onSubmit)}
2973 className={`${styles.Btn} bg-custom-blue text-white mb-6`}
2974 >
2975 Save and add another DSP
2976 </button>
2977 )}
2978 <button className={`${styles.Btn} bg-custom-red text-white mb-6`}>
2979 Save and close
2980 </button>
2981 </div>
2982 </form>
2983 </Modal>
2984 );
2985};
2986
2987export default PopUp;
2988import Modal from 'react-modal';
2989import { Link } from 'react-router-dom';
2990
2991import CrossIcon from '../../icons/CrossIcon';
2992import WarningPopUpIcon from '../../icons/WarningPopUpIcon';
2993import DeletePopUpIcon from '../../icons/DeletePopUpIcon';
2994
2995const styles = {
2996 Btn: `w-32 py-2 rounded-lg font-body text-14px select-none`,
2997};
2998
2999const modalStyles = {
3000 content: {
3001 height: 'auto',
3002 width: '430px',
3003 border: 'none',
3004 boxShadow: '0px 0px 10px 0px lightgray',
3005 top: '50%',
3006 left: '50%',
3007 right: 'auto',
3008 bottom: 'auto',
3009 marginRight: '-50%',
3010 transform: 'translate(-50%, -50%)',
3011 },
3012};
3013
3014const PopUpChoice = ({ isOpen, onClose, type, setNewTableData }) => {
3015 const onConfirm = () => {
3016 if (type === 'DELETE') {
3017 setNewTableData();
3018 }
3019 };
3020
3021 return (
3022 <Modal isOpen={isOpen} style={modalStyles} contentLabel="Example Modal">
3023 <div
3024 className="flex justify-end cursor-pointer"
3025 onClick={() => onClose()}
3026 >
3027 <CrossIcon />
3028 </div>
3029
3030 <div className="flex justify-center mb-6">
3031 <div className="h-20 w-20 bg-gray-100 flex items-center justify-center rounded-full text-center">
3032 <div>
3033 {type === 'CANCEL' && <WarningPopUpIcon />}
3034 {type === 'DELETE' && <DeletePopUpIcon />}
3035 </div>
3036 </div>
3037 </div>
3038
3039 {type === 'CANCEL' && (
3040 <>
3041 <div className="flex justify-center mb-2">
3042 <p className="w-330px text-center font-body text-20px text-custom-blue">
3043 Campaign was not saved!
3044 </p>
3045 </div>
3046
3047 <p className="text-center text-custom-blue text-opacity-70 text-16px mb-6 font-body">
3048 Are you sure that you want to cancel Campaign creation process?
3049 </p>
3050 </>
3051 )}
3052
3053 {type === 'DELETE' && (
3054 <div className="flex justify-center mb-6">
3055 <p className="w-330px text-center font-body text-20px text-custom-blue">
3056 Are you sure you wish to delete creative{' '}
3057 <span className="font-bold">creative 1</span>?
3058 </p>
3059 </div>
3060 )}
3061
3062 <div className="flex justify-center mb-6">
3063 <button
3064 type="button"
3065 className={`${styles.Btn} bg-custom-blue text-white mr-3`}
3066 onClick={() => onClose()}
3067 >
3068 <span className="px-8">No</span>
3069 </button>
3070 <Link
3071 to="/campaigns"
3072 className={`${styles.Btn} bg-custom-red text-white ml-3`}
3073 onClick={() => {
3074 onConfirm();
3075 onClose();
3076 }}
3077 >
3078 <span className="px-12">Yes</span>
3079 </Link>
3080 </div>
3081 </Modal>
3082 );
3083};
3084
3085export default PopUpChoice;
3086import Modal from 'react-modal';
3087
3088import CrossIcon from '../../icons/CrossIcon';
3089import SuccessPopUpIcon from '../../icons/SuccessPopUpIcon';
3090
3091const styles = {
3092 iconWrapper: `h-20 w-20 bg-gray-100 flex items-center justify-center rounded-full text-center`,
3093 title: `w-330px text-center font-body text-20px text-custom-blue`,
3094 btn: `w-32 py-2 rounded-lg font-body text-14px select-none`,
3095};
3096
3097const modalStyles = {
3098 content: {
3099 height: 'auto',
3100 width: '430px',
3101 border: 'none',
3102 boxShadow: '0px 0px 10px 0px lightgray',
3103 top: '50%',
3104 left: '50%',
3105 right: 'auto',
3106 bottom: 'auto',
3107 marginRight: '-50%',
3108 transform: 'translate(-50%, -50%)',
3109 },
3110};
3111
3112const PopUpSuccess = ({ isOpen, onClose }) => {
3113 return (
3114 <Modal isOpen={isOpen} style={modalStyles} contentLabel="Example Modal">
3115 <div
3116 className="flex justify-end cursor-pointer"
3117 onClick={() => onClose()}
3118 >
3119 <CrossIcon />
3120 </div>
3121
3122 <div className="flex justify-center mb-6">
3123 <div className={styles.iconWrapper}>
3124 <div>
3125 <SuccessPopUpIcon />
3126 </div>
3127 </div>
3128 </div>
3129
3130 <div className="flex justify-center mb-6">
3131 <p className={styles.title}>
3132 Campaign <span className="font-bold">Campaign Name</span> created
3133 successfully
3134 </p>
3135 </div>
3136
3137 <div className="flex justify-center mb-6">
3138 <button
3139 type="button"
3140 className={`${styles.btn} bg-custom-red text-white`}
3141 >
3142 <span className="px-8">Done</span>
3143 </button>
3144 </div>
3145 </Modal>
3146 );
3147};
3148
3149export default PopUpSuccess;
3150import { gql, useQuery } from '@apollo/client';
3151import { useHistory } from 'react-router';
3152
3153const GET_ADVERTIZERS = gql`
3154 query GetAdvertizers {
3155 advertizers {
3156 edges {
3157 node {
3158 id
3159 name
3160 }
3161 }
3162 }
3163 }
3164`;
3165
3166const GET_DSPS = gql`
3167 query GetDsps {
3168 dsps {
3169 edges {
3170 node {
3171 id
3172 name
3173 }
3174 }
3175 }
3176 }
3177`;
3178
3179const GET_SSPS = gql`
3180 query GetSsps {
3181 ssps {
3182 edges {
3183 node {
3184 id
3185 name
3186 }
3187 }
3188 }
3189 }
3190`;
3191
3192const GET_CAMPAIGN = gql`
3193 query GetCampaign($id: ID!) {
3194 campaign(id: $id) {
3195 id
3196 name
3197 advertizer {
3198 id
3199 name
3200 }
3201 createdAt
3202 creatives {
3203 edges {
3204 node {
3205 id
3206 name
3207 dsps {
3208 edges {
3209 node {
3210 id
3211 name
3212 status
3213 ssps {
3214 edges {
3215 node {
3216 id
3217 name
3218 }
3219 }
3220 }
3221 }
3222 }
3223 }
3224 }
3225 }
3226 }
3227 }
3228 }
3229`;
3230
3231export const useGetAdvertizersQuery = () => {
3232 return useQuery(GET_ADVERTIZERS);
3233};
3234
3235export const useGetDspsQuery = () => {
3236 return useQuery(GET_DSPS);
3237};
3238
3239export const useGetSspsQuery = () => {
3240 return useQuery(GET_SSPS);
3241};
3242
3243export const useGetCampaignQuery = ({ variables }) => {
3244 const history = useHistory();
3245 return useQuery(GET_CAMPAIGN, {
3246 skip: !variables.id,
3247 variables,
3248 onCompleted: data => {
3249 if (!data.campaign) {
3250 history.push('/campaigns');
3251 }
3252 },
3253 });
3254};
3255.ReactModal__Body--open {
3256 overflow: hidden;
3257}
3258
3259.ReactModal__Overlay {
3260 position: fixed;
3261 z-index: 999999;
3262 top: 0;
3263 left: 0;
3264 width: 100vw;
3265 height: 100vh;
3266 background: rgba(0, 0, 0, 0.5);
3267 display: flex;
3268 align-items: center;
3269 justify-content: center;
3270}
3271
3272.ReactModal__Content {
3273 background: white;
3274 width: 50rem;
3275 max-width: calc(100vw - 2rem);
3276 max-height: calc(100vh - 2rem);
3277 box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.25);
3278 overflow-y: auto;
3279 position: relative;
3280}
3281
3282tr:hover .cell-icon {
3283 opacity: 1;
3284}
3285export const dot = dotColor => ({
3286 alignItems: 'center',
3287 display: 'flex',
3288 marginLeft: 'auto',
3289
3290 ':after': {
3291 backgroundColor: dotColor,
3292 borderRadius: 10,
3293 content: '" "',
3294 display: 'block',
3295 height: 6,
3296 width: 6,
3297 position: 'absolute',
3298 right: 16,
3299 },
3300});
3301
3302export const selectStyles = {
3303 control: (base, { isFocused, isDisabled }) => ({
3304 ...base,
3305 border: isFocused && '1px solid #1e2f59',
3306 borderRadius: 8,
3307 background: isDisabled ? 'rgba(0, 0, 0, 0.05)' : '#fff',
3308 minWidth: selectStyles.minWidth,
3309 width: selectStyles.width,
3310 }),
3311
3312 menu: base => ({
3313 ...base,
3314 borderRadius: 8,
3315 background: '#fff',
3316 width: selectStyles.minWidth,
3317 }),
3318
3319 singleValue: (base, { isDisabled }) => ({
3320 ...base,
3321 color: isDisabled ? 'rgba(30, 47, 89, 0.6)' : '#1e2f59',
3322 }),
3323
3324 menuList: base => ({
3325 ...base,
3326 color: '#78829c',
3327 }),
3328
3329 option: (base, { data, isFocused }) => ({
3330 ...base,
3331 ...dot(data.color),
3332 paddingLeft: 10,
3333 paddingTop: 5,
3334 paddingBottom: 5,
3335 color: isFocused && '#1e2f59',
3336 }),
3337};
3338
3339const _selectStyles = selectStyles;
3340_selectStyles.minWidth = '16rem';
3341_selectStyles.width = 'fit-content';
3342export const MultipleSelectStyles = _selectStyles;
3343
3344/*
3345const _modal_selectStyles = selectStyles;
3346_modal_selectStyles.minWidth = '100%';
3347_modal_selectStyles.width = 'fit-content';
3348export const ModalMultipleSelectStyles = _modal_selectStyles;
3349*/
3350
3351export const selectRowsStyles = {
3352 control: base => ({
3353 ...base,
3354 border: 'none',
3355 fontSize: '12px',
3356 height: '10px',
3357 color: '#48658f',
3358 fontFamily: 'Inter',
3359 marginTop: 2,
3360 }),
3361
3362 menu: base => ({
3363 ...base,
3364 borderRadius: 8,
3365 background: '#fff',
3366 }),
3367
3368 menuList: base => ({
3369 ...base,
3370 color: '#78829c',
3371 }),
3372
3373 option: (base, { isFocused }) => ({
3374 ...base,
3375 fontSize: '12px',
3376 fontFamily: 'Inter',
3377 color: isFocused && '#48658f',
3378 }),
3379};
3380module.exports = {
3381 future: {
3382 // removeDeprecatedGapUtilities: true,
3383 // purgeLayersByDefault: true,
3384 },
3385 purge: [],
3386 theme: {
3387 extend: {
3388 colors: {
3389 'main': '#f2efef',
3390 'table': '#f5f4f4',
3391 'table-border': '#e5e5e5',
3392
3393 'table-list-font': '#48658f',
3394
3395 'custom-red': '#e20d21',
3396 'custom-green': '#03bd5b',
3397 'custom-yellow': '#f5a623',
3398 'custom-blue': '#1e2f59',
3399
3400 'sidebar-btn': '#f7f6f6',
3401 'sidebar-font-btn': '#1e2f59',
3402
3403 'nav-link': '#69788d',
3404
3405 'disabled': '#fafafa',
3406 'disabled-input': '#e6e3e3',
3407 },
3408
3409 width: {
3410 '1/8': '12.5%',
3411 '1/24': '4.25%',
3412
3413 '330px': '330px',
3414 },
3415
3416 fontFamily: {
3417 'body': ['Inter', 'sans-serif'],
3418 },
3419
3420 fontSize: {
3421 '20px': '20px',
3422 '16px': '16px',
3423 '15px': '15px',
3424 '14px': '14px',
3425 '13px': '13px',
3426 '12px': '12px',
3427 },
3428
3429 textOpacity: {
3430 '70': '0.7',
3431 '60': '0.6',
3432 },
3433
3434 placeholderOpacity: {
3435 '60': '0.6',
3436 },
3437
3438 backgroundOpacity: {
3439 '8': '0.08',
3440 '5': '0.05',
3441 },
3442
3443 maxWidth: {
3444 'xs/2': '10rem',
3445 },
3446
3447 minWidth: {
3448 'xl': '150px',
3449 },
3450 },
3451 },
3452 variants: {},
3453 plugins: [
3454 require('@tailwindcss/custom-forms'),
3455 ],
3456}
3457