diff options
Diffstat (limited to 'src')
76 files changed, 10457 insertions, 0 deletions
diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..e436ecc --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,118 @@ +import NavBar from '@/views/components/NavBar' +import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy' +import { useEffect, useState } from 'react' +import { Outlet } from 'react-router-dom' +import { useRegisterSW } from 'virtual:pwa-register/react' +import { UserContext } from './contexts/UserContext' +import { AuthenticationProvider } from './service/AuthenticationService' +import { GetUserProfile } from './utils/Fetcher' +import { isTokenValid } from './utils/TokenManager' + +const add = className => { + document.getElementById('root').classList.add(className) +} + +const remove = className => { + document.getElementById('root').classList.remove(className) +} +// TODO: Update the interval to at 60 minutes +const intervalMS = 5 * 60 * 1000 // 5 minutes + +function App() { + const { mode, systemMode } = useColorScheme() + const [userProfile, setUserProfile] = useState(null) + const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(true) + + const { + offlineReady: [offlineReady, setOfflineReady], + needRefresh: [needRefresh, setNeedRefresh], + updateServiceWorker, + } = useRegisterSW({ + onRegistered(r) { + // eslint-disable-next-line prefer-template + console.log('SW Registered: ' + r) + r && + setInterval(() => { + r.update() + }, intervalMS) + }, + onRegisterError(error) { + console.log('SW registration error', error) + }, + }) + const close = () => { + setOfflineReady(false) + setNeedRefresh(false) + } + + // const updateServiceWorker = useRegisterSW({ + // onRegistered(r) { + // r && + // setInterval(() => { + // r.update() + // }, intervalMS) + // }, + // }) + const setThemeClass = () => { + const value = JSON.parse(localStorage.getItem('themeMode')) || mode + + if (value === 'system') { + if (systemMode === 'dark') { + return add('dark') + } + return remove('dark') + } + + if (value === 'dark') { + return add('dark') + } + + return remove('dark') + } + const getUserProfile = () => { + GetUserProfile() + .then(res => { + res.json().then(data => { + setUserProfile(data.res) + }) + }) + .catch(error => {}) + } + useEffect(() => { + setThemeClass() + }, [mode, systemMode]) + useEffect(() => { + if (isTokenValid()) { + if (!userProfile) getUserProfile() + } + }, []) + + return ( + <div className='min-h-screen'> + <AuthenticationProvider /> + <UserContext.Provider value={{ userProfile, setUserProfile }}> + <NavBar /> + <Outlet /> + </UserContext.Provider> + {needRefresh && ( + <Snackbar open={showUpdateSnackbar}> + <Typography level='body-md'> + A new version is now available.Click on reload button to update. + </Typography> + <Button + color='secondary' + size='small' + onClick={() => { + updateServiceWorker(true) + setShowUpdateSnackbar(false) + }} + > + Refresh + </Button> + </Snackbar> + )} + </div> + ) +} + +export default App diff --git a/src/Config.js b/src/Config.js new file mode 100644 index 0000000..19a896b --- /dev/null +++ b/src/Config.js @@ -0,0 +1,5 @@ +/* eslint-env node */ +export const API_URL = import.meta.env.VITE_APP_API_URL //|| 'http://localhost:8000' +export const REDIRECT_URL = import.meta.env.VITE_APP_REDIRECT_URL //|| 'http://localhost:3000' +export const GOOGLE_CLIENT_ID = import.meta.env.VITE_APP_GOOGLE_CLIENT_ID +export const ENVIROMENT = import.meta.env.VITE_APP_ENVIROMENT diff --git a/src/Logo.jsx b/src/Logo.jsx new file mode 100644 index 0000000..664c607 --- /dev/null +++ b/src/Logo.jsx @@ -0,0 +1,9 @@ +import LogoSVG from '@/assets/logo.svg' +const Logo = () => { + return ( + <div className='logo'> + <img src={LogoSVG} alt='logo' width='128px' height='128px' /> + </div> + ) +} +export default Logo diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..f205a47 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,1185 @@ +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + width="100%" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve"> + +<path fill="#B5BEFB" opacity="1.000000" stroke="none" + d=" +M457.067200,42.686905 + C459.447144,26.832342 469.528015,16.203293 481.577301,7.055771 + C484.583862,4.773226 488.270081,3.385903 491.321960,1.293556 + C503.687561,1.000000 516.375122,1.000000 529.531372,1.000004 + C532.877991,2.985393 535.601196,5.258084 538.660706,6.906528 + C552.035889,14.112956 561.123352,24.814444 565.647583,39.326729 + C566.080750,40.716061 566.044800,42.251637 566.534424,44.416672 + C566.269714,53.474617 565.697205,61.835217 565.124695,70.195816 + C562.884399,82.024635 556.006897,90.798393 546.797058,98.042542 + C545.892578,98.753990 545.048767,99.542503 543.702881,100.681946 + C542.668823,101.562706 542.108643,102.056770 540.874512,103.145233 + C543.896545,103.378769 546.007507,103.627136 548.124084,103.692657 + C579.398621,104.660835 610.777283,101.454941 641.961609,106.044365 + C641.961609,106.044365 641.997559,106.000000 642.232666,106.281258 + C643.636841,106.730606 644.805908,106.898689 645.975037,107.066772 + C645.975037,107.066772 646.399109,107.196709 646.814575,107.547272 + C648.802185,108.297722 650.374390,108.697617 651.946533,109.097511 + C651.946533,109.097511 652.334167,109.251457 652.673218,109.635696 + C655.649536,111.048347 658.286804,112.076759 660.924011,113.105164 + C660.924011,113.105164 661.338745,113.293716 661.638184,113.612358 + C662.257812,113.993759 662.577942,114.056519 662.898132,114.119278 + C662.898132,114.119278 663.313416,114.351051 663.616821,114.670654 + C663.920227,114.990265 664.358093,115.225540 664.358093,115.225540 + C664.358093,115.225540 664.843811,115.120506 664.843811,115.120506 + C664.843811,115.120506 664.924805,115.072365 664.949219,115.443268 + C665.933105,116.254326 666.892517,116.694481 667.851990,117.134628 + C667.851990,117.134621 667.947266,117.053314 667.981079,117.440826 + C669.021606,118.253540 670.028381,118.678749 671.271301,119.170792 + C671.824585,119.286629 672.140808,119.341209 672.622070,119.696716 + C679.759644,127.731895 687.501282,134.920029 693.530396,143.335159 + C703.686951,157.510941 709.115906,173.618210 708.473145,191.277771 + C681.710510,191.293823 655.410950,191.385529 629.111389,191.401764 + C529.069275,191.463501 429.027130,191.500854 328.965271,191.124725 + C330.406860,167.369614 339.520386,147.529587 356.163544,131.138840 + C359.916351,127.442932 363.727966,123.806702 367.806641,120.045158 + C369.087769,118.981110 370.074799,118.014900 371.061859,117.048691 + C371.061859,117.048691 371.013489,116.999557 371.355469,117.107140 + C371.996918,116.890915 372.296387,116.567116 372.595886,116.243324 + C372.595886,116.243332 372.563263,116.098564 373.016327,116.232864 + C373.845886,115.978600 374.222382,115.590034 374.598877,115.201477 + C374.598877,115.201469 374.529510,115.009590 374.926147,115.148865 + C375.885895,114.907318 376.448944,114.526497 377.012024,114.145676 + C383.126526,111.437981 389.241028,108.730270 396.026550,106.213028 + C424.273346,100.114319 452.383545,104.830887 480.419342,102.608955 + C479.972687,101.906181 479.796143,101.424187 479.455841,101.134781 + C478.702362,100.494049 477.866638,99.950081 477.064423,99.366669 + C472.619659,94.664429 467.385376,90.467857 463.956818,85.114799 + C460.664032,79.973732 459.242096,73.634453 456.975769,66.983047 + C456.991394,58.325230 457.029297,50.506069 457.067200,42.686905 +M495.040314,43.670990 + C486.376251,56.938816 493.815094,72.260345 507.730530,74.933403 + C517.515564,76.813034 529.649231,68.062172 531.328369,59.447845 + C532.796204,51.917713 530.302063,46.677364 526.017822,41.704769 + C518.169128,32.594967 503.036163,33.744705 495.040314,43.670990 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M367.512543,120.143005 + C363.727966,123.806702 359.916351,127.442932 356.163544,131.138840 + C339.520386,147.529587 330.406860,167.369614 328.768921,191.520508 + C328.478699,198.661301 328.300964,204.983322 328.263184,211.306168 + C328.132141,233.240723 334.086792,239.255035 355.925262,239.252853 + C463.741425,239.242065 571.557556,239.221786 679.373718,239.197357 + C683.039307,239.196518 686.712097,239.247787 690.369019,239.046799 + C700.273621,238.502396 708.806946,230.141632 709.049622,220.303772 + C709.287842,210.648895 708.995605,200.980957 708.936157,191.318741 + C709.115906,173.618210 703.686951,157.510941 693.530396,143.335159 + C687.501282,134.920029 679.759644,127.731895 673.147339,119.643036 + C691.453796,119.205421 709.400879,119.116837 727.348022,119.028259 + C743.718384,119.185036 759.522888,122.356789 774.749939,128.267838 + C786.922852,132.993240 797.822937,139.913391 808.099548,148.875977 + C808.958862,149.589859 809.502563,149.758560 810.046326,149.927246 + C810.046326,149.927246 810.021057,149.975540 810.086792,150.276672 + C810.779724,151.029556 811.406799,151.481308 812.033936,151.933075 + C812.033936,151.933060 812.019409,151.965164 812.138550,152.173523 + C812.441711,152.643311 812.684631,152.826950 812.986328,152.932816 + C812.986328,152.932816 813.059509,152.936432 813.168884,153.175323 + C813.468811,153.665741 813.713806,153.841400 814.013184,153.941177 + C814.013184,153.941177 814.051208,153.950363 814.088745,154.311172 + C818.032654,158.893280 821.994446,163.064835 825.836121,167.344177 + C846.403625,190.254852 856.329895,217.527451 856.795593,247.912750 + C857.175781,272.717957 857.808350,297.541016 854.362427,322.368683 + C852.379761,336.653229 853.979675,351.360748 856.036316,365.862640 + C857.653748,377.267303 859.067017,388.958374 853.599670,400.041992 + C852.912598,401.434784 852.977539,403.305450 853.038879,404.941803 + C853.287476,411.573303 853.994141,418.202515 853.938171,424.827576 + C853.815247,439.383301 853.345520,453.936340 852.972229,468.489319 + C852.938599,469.801819 852.610535,471.112061 852.363953,472.412842 + C850.920166,480.029877 848.397461,487.615387 848.193176,495.263733 + C847.398071,525.033203 847.340332,554.822327 846.994141,584.603882 + C846.972412,586.474854 846.830505,588.344421 846.744507,590.214600 + C846.145996,590.232544 845.547485,590.250549 844.948975,590.268494 + C844.658142,587.152283 844.109802,584.033447 844.141357,580.920532 + C844.181458,576.954102 844.996338,572.987915 844.935974,569.029785 + C844.796814,559.899963 844.151367,550.777222 844.039978,541.647644 + C843.875366,528.157349 844.023315,514.663452 843.973206,501.171417 + C843.962769,498.366394 844.243896,495.191406 839.997925,494.538177 + C839.326233,478.318695 839.758423,462.430847 837.779663,446.849182 + C834.529297,421.255829 823.151123,398.918518 805.493042,380.181274 + C793.981995,367.966766 781.588867,356.570251 769.352356,345.060852 + C765.653015,341.581299 761.155518,338.950317 756.843567,335.649292 + C743.908691,326.317169 729.995605,319.814392 714.687195,316.457092 + C713.554565,316.208710 712.242615,316.778168 710.639099,316.950745 + C698.542175,312.221039 686.391602,309.916016 673.730347,309.923553 + C565.775574,309.987640 457.820831,309.968811 349.866089,310.059326 + C344.561615,310.063751 339.193878,310.469238 333.963470,311.337677 + C280.783264,320.168030 243.279373,364.230225 243.023254,418.147522 + C242.903763,443.303101 242.982635,468.459686 242.985275,493.615814 + C242.994949,586.243652 242.955917,678.871582 243.061478,771.499329 + C243.088242,794.982666 249.616028,816.593689 263.836487,835.442505 + C268.606262,841.764709 274.585938,847.174133 279.898254,853.120605 + C279.604553,853.562805 279.704010,853.814636 280.044067,853.997742 + C280.005829,853.999634 280.002563,853.923096 280.186279,854.166626 + C280.937561,854.595703 281.505157,854.781189 282.037994,854.970215 + C282.003235,854.973633 282.046844,854.919006 282.027588,855.286072 + C283.158875,857.410828 284.116425,859.338989 285.492432,860.897583 + C292.535645,868.875305 299.851685,876.616882 306.761627,884.706421 + C317.245636,896.980347 329.179504,907.451233 343.404388,915.154358 + C365.429352,927.081238 388.899506,932.710754 413.902679,931.339478 + C408.122437,933.121399 402.442688,934.550720 396.761139,935.972961 + C390.791687,937.467468 385.126617,937.440063 379.376343,934.430664 + C374.882202,932.078674 369.887848,929.649963 364.994995,929.370667 + C359.659149,929.066101 355.466064,927.296631 351.280640,924.582336 + C350.536652,924.099854 349.671295,923.804443 347.977051,923.005249 + C349.180267,925.916565 350.009216,927.922302 350.923737,930.135132 + C345.875610,932.306213 339.497589,930.720032 336.486237,936.514343 + C336.331818,936.811401 335.562317,936.919800 335.088043,936.900391 + C324.405731,936.462463 313.717834,936.111389 303.045990,935.486938 + C296.836243,935.123596 290.664795,934.695129 285.914490,939.670166 + C283.610229,942.083435 281.634979,942.063660 278.927979,940.412354 + C276.570862,938.974487 273.935120,937.459961 271.281799,937.137634 + C262.104797,936.022888 253.458191,933.694641 245.492767,928.815491 + C238.599548,924.593079 231.271408,921.058594 224.521591,916.631348 + C219.204956,913.144165 214.256332,909.001465 209.550766,904.704773 + C205.226151,900.755737 201.842072,895.720215 197.316742,892.062683 + C192.335907,888.036987 190.979248,882.060059 187.697311,877.163818 + C185.246872,873.508179 183.672867,869.253052 181.818619,865.214294 + C180.663101,862.697388 178.861938,860.101013 178.817535,857.515076 + C178.628326,846.496338 173.810349,837.184875 168.344193,827.855835 + C167.921219,832.154236 167.309845,836.566223 168.032440,840.747681 + C170.493652,854.990051 175.235184,868.571350 181.171188,881.728210 + C182.196503,884.000732 182.235214,885.558777 179.185074,885.971924 + C174.280182,873.505493 169.628265,861.253113 164.956284,848.705566 + C164.636017,848.262268 164.335815,848.114014 164.035629,847.965759 + C164.035629,847.965759 164.018539,847.998840 164.312347,847.753601 + C164.076660,845.324341 163.547180,843.140381 163.017685,840.956360 + C163.017685,840.956360 163.043472,840.974731 163.221771,840.560669 + C163.123734,837.398071 162.847382,834.649475 162.610870,831.475159 + C162.540268,823.223755 162.330643,815.398010 162.334015,807.572449 + C162.415344,619.038269 162.519424,430.504120 162.764160,241.573059 + C164.396896,224.044144 169.042175,207.829910 176.951843,192.546234 + C177.533127,191.422989 177.651611,190.060211 177.985229,188.808792 + C180.320938,185.255600 182.656647,181.702408 185.368835,177.754288 + C188.851440,173.245544 191.957565,169.131729 195.063690,165.017914 + C195.063675,165.017914 195.003845,165.003922 195.364883,165.080093 + C199.946686,161.225510 204.181549,157.309738 208.382980,153.358398 + C210.961136,150.933762 213.488052,148.454681 216.037979,146.000031 + C216.037979,146.000031 216.029800,146.033829 216.361084,146.145721 + C217.813522,145.179688 218.934677,144.101730 220.055832,143.023773 + C220.055832,143.023773 220.000656,143.001984 220.359589,143.120880 + C222.158813,142.152878 223.599106,141.065979 225.039383,139.979095 + C225.039383,139.979095 225.032379,140.038437 225.349579,140.081543 + C226.460587,139.416077 227.254410,138.707520 228.048218,137.998962 + C228.048218,137.998962 228.037643,138.048889 228.387024,138.114716 + C233.485779,135.763092 238.235138,133.345642 243.359009,130.899567 + C250.604446,128.297592 257.475372,125.724258 264.346283,123.150917 + C264.346283,123.150917 264.479218,123.093704 264.901001,123.138603 + C265.551697,123.137711 265.780579,123.091919 266.430298,123.052216 + C274.237335,121.799263 281.623535,120.540222 289.476929,119.317017 + C314.376343,119.306656 338.808563,119.244797 363.240784,119.254318 + C364.664825,119.254875 366.088623,119.833435 367.512543,120.143005 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M840.018799,495.007324 + C844.243896,495.191406 843.962769,498.366394 843.973206,501.171417 + C844.023315,514.663452 843.875366,528.157349 844.039978,541.647644 + C844.151367,550.777222 844.796814,559.899963 844.935974,569.029785 + C844.996338,572.987915 844.181458,576.954102 844.141357,580.920532 + C844.109802,584.033447 844.658142,587.152283 844.948975,590.268494 + C845.547485,590.250549 846.145996,590.232544 846.744507,590.214600 + C846.830505,588.344421 846.972412,586.474854 846.994141,584.603882 + C847.340332,554.822327 847.398071,525.033203 848.193176,495.263733 + C848.397461,487.615387 850.920166,480.029877 852.363953,472.412842 + C852.610535,471.112061 852.938599,469.801819 852.972229,468.489319 + C853.345520,453.936340 853.815247,439.383301 853.938171,424.827576 + C853.994141,418.202515 853.287476,411.573303 853.038879,404.941803 + C852.977539,403.305450 852.912598,401.434784 853.599670,400.041992 + C859.067017,388.958374 857.653748,377.267303 856.036316,365.862640 + C853.979675,351.360748 852.379761,336.653229 854.362427,322.368683 + C857.808350,297.541016 857.175781,272.717957 856.795593,247.912750 + C856.329895,217.527451 846.403625,190.254852 825.836121,167.344177 + C821.994446,163.064835 818.032654,158.893280 814.095337,154.329620 + C820.264893,160.224182 826.899231,166.093796 832.564697,172.783936 + C841.113281,182.878738 846.997009,194.774704 851.359863,207.113754 + C854.489685,215.965591 855.696167,225.527374 857.407471,234.837357 + C858.177551,239.026825 858.343750,243.374557 858.346741,247.650787 + C858.401428,326.463135 858.385925,405.275482 858.399048,484.087860 + C858.400269,491.249817 858.513855,498.411652 858.543823,505.573700 + C858.844360,577.417786 859.186035,649.261780 859.413391,721.106079 + C859.523010,755.761902 859.915344,790.429138 859.178833,825.069885 + C858.842896,840.865723 855.393066,856.446045 849.288391,871.254150 + C839.527588,894.930969 823.921387,914.099121 803.508240,929.201782 + C795.927734,934.810120 787.249023,939.034180 778.781982,943.318115 + C768.792419,948.372375 758.154053,951.665405 746.162720,952.788208 + C744.932739,952.769287 744.469482,952.730286 744.004517,952.517944 + C744.002747,952.344543 744.002441,951.997559 744.390686,951.930542 + C770.306152,947.103455 792.922974,936.492004 812.617310,919.343201 + C829.715393,904.455261 841.866272,886.435974 849.380493,865.190491 + C851.441040,859.364441 854.719177,853.565063 851.139221,847.075500 + C850.650146,846.188904 850.874817,844.733948 851.141113,843.632019 + C854.427124,830.033813 850.615173,817.046753 847.751892,803.987915 + C847.554016,803.085510 846.639954,802.340088 845.998352,801.436340 + C843.891052,803.736511 845.286621,809.205322 839.863281,806.469971 + C839.903625,797.339233 839.957275,788.672791 839.959473,780.006409 + C839.983948,685.006714 840.000244,590.007019 840.018799,495.007324 +M856.997559,549.500000 + C856.997559,536.177307 856.997559,522.854614 856.997559,509.531891 + C856.742004,509.535126 856.486450,509.538391 856.230896,509.541626 + C856.230896,577.883972 856.230896,646.226257 856.230896,714.568542 + C856.486450,714.569092 856.742004,714.569580 856.997559,714.570068 + C856.997559,659.880066 856.997559,605.190002 856.997559,549.500000 +M855.997559,445.500366 + C855.997559,459.519104 855.997559,473.537842 855.997559,487.556549 + C856.213379,487.553558 856.429199,487.550537 856.645020,487.547516 + C856.645020,467.245667 856.645020,446.943817 856.645020,426.641968 + C856.429199,426.641449 856.213379,426.640930 855.997559,426.640411 + C855.997559,432.593872 855.997559,438.547302 855.997559,445.500366 +M855.997498,808.994690 + C856.085205,810.154968 856.172852,811.315247 856.260498,812.475464 + C856.419250,812.461060 856.578003,812.446655 856.736755,812.432251 + C856.736755,793.093262 856.736755,773.754272 856.736755,754.415222 + C856.490356,754.414307 856.243958,754.413391 855.997559,754.412415 + C855.997559,772.313599 855.997559,790.214783 855.997498,808.994690 +M844.997559,602.588379 + C844.997559,604.428589 844.997559,606.268738 844.997559,608.108948 + C845.444946,608.083740 845.892334,608.058594 846.339722,608.033386 + C846.339722,604.664795 846.339722,601.296143 846.339722,597.927551 + C846.011902,597.922180 845.684021,597.916809 845.356201,597.911438 + C845.236633,599.185974 845.117126,600.460510 844.997559,602.588379 +M856.030396,420.843170 + C856.155884,421.118500 856.281372,421.393799 856.406860,421.669098 + C856.518494,421.615692 856.726440,421.564087 856.727539,421.508575 + C856.747070,420.487610 856.741760,419.466217 856.741760,418.444885 + C856.542786,418.444489 856.343811,418.444092 856.144897,418.443726 + C856.098877,419.054138 856.052856,419.664551 856.030396,420.843170 +z"/> +<path fill="#637E63" opacity="1.000000" stroke="none" + d=" +M744.006287,952.691284 + C744.469482,952.730286 744.932739,952.769287 745.739624,952.877441 + C741.887573,953.948425 737.692993,955.819275 733.495789,955.825012 + C622.607300,955.977966 511.718536,955.925293 400.829803,955.938599 + C362.204681,955.943237 323.579346,956.101257 284.954834,955.969238 + C280.997284,955.955750 277.044922,954.428162 273.326263,953.260864 + C297.713715,953.297180 321.865051,953.675781 346.479370,954.119751 + C381.235168,954.164368 415.527924,954.122131 449.820709,954.126892 + C540.214294,954.139587 630.607910,954.192749 721.001465,954.150757 + C726.779480,954.148071 732.557312,953.471008 738.494019,953.152161 + C738.652832,953.196716 738.962341,953.082825 739.385437,953.109375 + C741.207764,952.987732 742.607056,952.839539 744.006287,952.691284 +z"/> +<path fill="#06b6d4" opacity="1.000000" stroke="none" + d=" +M162.619415,241.969971 + C162.519424,430.504120 162.415344,619.038269 162.334015,807.572449 + C162.330643,815.398010 162.540268,823.223755 162.438629,831.472839 + C162.226593,831.896301 161.889160,831.966003 161.889160,831.966003 + C161.592010,826.450500 161.246964,820.936951 161.017670,815.418579 + C160.900940,812.609009 160.992310,809.790710 160.987473,806.040527 + C160.990158,722.842468 161.000259,640.580322 160.964508,558.318115 + C160.963745,556.565186 160.435410,554.812500 160.153198,553.059692 + C160.112396,550.988770 160.071609,548.917847 160.337311,546.068665 + C160.760559,544.471375 160.978836,543.652466 160.979370,542.833374 + C160.997757,514.500916 161.006149,486.168457 160.961838,457.836121 + C160.959839,456.563782 160.453461,455.292236 160.182007,454.020325 + C160.112488,423.247040 160.009888,392.473785 159.981750,361.700470 + C159.945847,322.438934 159.951889,283.177307 160.025192,243.915878 + C160.028671,242.056046 160.859131,240.197739 161.656052,238.478958 + C162.211578,239.736115 162.415497,240.853043 162.619415,241.969971 +z"/> +<path fill="#00FFFF" opacity="1.000000" stroke="none" + d=" +M160.084442,553.520630 + C160.435410,554.812500 160.963745,556.565186 160.964508,558.318115 + C161.000259,640.580322 160.990158,722.842468 160.966766,805.575073 + C160.632248,804.539062 160.040527,803.032654 160.039810,801.526062 + C160.000397,719.011230 160.010559,636.496460 160.084442,553.520630 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M346.016357,954.054382 + C321.865051,953.675781 297.713715,953.297180 273.080811,952.937500 + C272.599182,952.956421 272.260376,952.915649 271.814270,952.691528 + C270.052246,952.081604 268.736267,951.695740 267.846802,951.220337 + C273.580353,948.060608 279.073853,950.637390 284.459656,950.717957 + C290.397705,950.806763 296.328369,951.926453 302.258606,951.895813 + C315.678070,951.826538 329.094482,951.260254 342.514587,951.093689 + C344.001587,951.075256 345.504486,952.336853 346.796021,953.139404 + C346.335419,953.486328 346.143494,953.746460 346.016357,954.054382 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M641.746216,105.740341 + C610.777283,101.454941 579.398621,104.660835 548.124084,103.692657 + C546.007507,103.627136 543.896545,103.378769 540.874512,103.145233 + C542.108643,102.056770 542.668823,101.562706 543.529297,100.978439 + C545.653198,101.292381 547.475647,102.042229 549.300537,102.048439 + C573.962708,102.132462 598.628418,101.899818 623.285339,102.270638 + C629.381653,102.362328 635.449768,104.330650 641.746216,105.740341 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M164.976349,849.000671 + C169.628265,861.253113 174.280182,873.505493 179.244354,886.247498 + C179.853500,887.211243 180.150436,887.685425 180.564713,888.501587 + C182.407700,891.398071 184.133316,893.952515 185.871307,896.666260 + C185.883682,896.825439 186.124237,897.035461 186.173950,897.272583 + C186.368225,897.833008 186.614944,898.025513 186.973724,898.053894 + C186.983658,898.020691 186.914673,898.026978 186.989227,898.323853 + C187.348221,898.908630 187.632660,899.196533 187.921509,899.647583 + C187.925919,899.810669 188.155853,900.042297 188.202271,900.269287 + C188.407745,900.791992 188.653412,900.964111 188.992844,901.006348 + C189.000000,901.000000 188.987137,901.013733 189.072021,901.271606 + C189.416840,901.776672 189.676758,902.023987 189.986023,902.537720 + C190.376892,903.208923 190.718445,903.613770 191.132080,904.248657 + C191.392334,904.765930 191.655960,904.936646 191.995972,904.996582 + C191.996902,905.002441 192.008835,905.005249 192.088135,905.246460 + C192.436050,905.677795 192.704681,905.867920 192.985931,906.030884 + C192.998520,906.003723 192.942551,906.024902 193.026917,906.286377 + C193.386230,906.783020 193.661179,907.018188 194.038177,907.552002 + C195.422150,909.231506 196.704071,910.612366 198.065399,912.238525 + C198.424850,912.666321 198.704941,912.848755 198.991333,913.014404 + C198.997620,912.997559 198.971634,913.023621 199.109039,913.318115 + C200.837189,915.060181 202.427948,916.507690 204.009216,917.979614 + C203.999710,918.003967 204.050385,917.991028 204.131653,918.220581 + C204.393250,918.747742 204.654968,918.925659 204.996948,918.994019 + C204.995773,919.004211 205.016068,919.001587 205.102859,919.234253 + C205.384537,919.757385 205.652847,919.940186 206.087311,920.246948 + C206.609146,920.700745 207.038300,920.922852 207.658997,921.424683 + C209.107803,922.913208 210.365097,924.121948 211.452667,925.513550 + C211.282928,925.696411 210.943954,926.062683 210.943939,926.062683 + C206.682892,922.596985 201.948410,919.565918 198.273193,915.562439 + C192.928619,909.740662 187.658661,903.631897 183.609131,896.893005 + C178.115723,887.751404 173.389374,878.078918 169.146606,868.282227 + C166.637726,862.489136 165.696335,856.017273 164.316650,849.426147 + C164.583221,849.001587 164.976349,849.000671 164.976349,849.000671 +z"/> +<path fill="#2CC4AC" opacity="1.000000" stroke="none" + d=" +M477.001221,99.704071 + C477.866638,99.950081 478.702362,100.494049 479.455841,101.134781 + C479.796143,101.424187 479.972687,101.906181 480.419342,102.608955 + C452.383545,104.830887 424.273346,100.114319 396.427979,106.251633 + C398.961578,105.079697 401.693359,103.568764 404.583801,103.148270 + C409.605164,102.417793 414.722748,102.103699 419.803162,102.068298 + C437.947723,101.941887 456.093781,102.035713 474.238251,101.903366 + C475.076782,101.897263 478.206116,103.684158 477.001221,99.704071 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M162.764160,241.573059 + C162.415497,240.853043 162.211578,239.736115 162.000977,238.265656 + C163.086914,225.607986 164.658722,213.449402 170.869873,202.393097 + C173.269608,198.121384 175.208389,193.590683 177.671204,188.993378 + C177.651611,190.060211 177.533127,191.422989 176.951843,192.546234 + C169.042175,207.829910 164.396896,224.044144 162.764160,241.573059 +z"/> +<path fill="#00FF99" opacity="1.000000" stroke="none" + d=" +M160.113678,454.484558 + C160.453461,455.292236 160.959839,456.563782 160.961838,457.836121 + C161.006149,486.168457 160.997757,514.500916 160.979370,542.833374 + C160.978836,543.652466 160.760559,544.471375 160.412903,545.630859 + C160.136429,515.630432 160.090897,485.289642 160.113678,454.484558 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M367.806641,120.045166 + C366.088623,119.833435 364.664825,119.254875 363.240784,119.254318 + C338.808563,119.244797 314.376343,119.306656 289.466949,119.043747 + C288.965912,118.554092 288.942047,118.373535 288.918152,118.192978 + C310.462250,118.128761 332.006836,118.127151 353.550110,117.957306 + C359.096130,117.913589 364.637970,117.340462 370.621796,117.029617 + C370.074799,118.014900 369.087769,118.981110 367.806641,120.045166 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M215.668030,145.999268 + C213.488052,148.454681 210.961136,150.933762 208.382980,153.358398 + C204.181549,157.309738 199.946686,161.225510 195.377960,165.107895 + C197.868362,161.645493 200.404190,157.906754 203.626160,154.904373 + C207.189178,151.584167 211.380432,148.938141 215.668030,145.999268 +z"/> +<path fill="#449678" opacity="1.000000" stroke="none" + d=" +M727.067993,118.662300 + C709.400879,119.116837 691.453796,119.205421 672.981384,119.347687 + C672.140808,119.341209 671.824585,119.286629 671.263428,118.966019 + C671.019348,118.694420 671.004333,118.285271 671.004333,118.285271 + C689.598877,118.288956 708.193420,118.292648 727.067993,118.662300 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M267.420319,951.309875 + C268.736267,951.695740 270.052246,952.081604 271.647125,952.657349 + C261.184357,952.121094 250.886978,949.728455 241.210999,944.257202 + C241.124313,943.833984 241.302032,943.614014 241.673523,943.691711 + C247.645523,945.586548 253.246033,947.403687 258.878174,949.201233 + C258.909851,949.181641 258.859283,949.126953 259.050751,949.351562 + C259.811005,949.668213 260.379791,949.760193 261.184509,949.979858 + C261.770721,950.109863 262.120941,950.112305 262.725647,950.340088 + C263.635315,950.658508 264.290497,950.751526 265.240295,950.977417 + C266.163361,951.176880 266.791840,951.243347 267.420319,951.309875 +z"/> +<path fill="#4CD298" opacity="1.000000" stroke="none" + d=" +M263.904022,123.090408 + C257.475372,125.724258 250.604446,128.297592 243.360748,130.718353 + C242.987946,130.565765 242.990158,130.203278 242.990158,130.203278 + C249.814026,127.812149 256.637878,125.421021 263.904022,123.090408 +z"/> +<path fill="#5CCD95" opacity="1.000000" stroke="none" + d=" +M288.503784,118.129204 + C288.942047,118.373535 288.965912,118.554092 288.999756,119.007919 + C281.623535,120.540222 274.237335,121.799263 266.433289,122.843079 + C266.015411,122.627853 266.037292,122.210213 266.037292,122.210220 + C273.388000,120.828621 280.738739,119.447021 288.503784,118.129204 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M242.617218,130.138428 + C242.990158,130.203278 242.987946,130.565765 242.986221,130.746979 + C238.235138,133.345642 233.485779,135.763092 228.361542,138.113708 + C232.739182,135.389130 237.491730,132.731339 242.617218,130.138428 +z"/> +<path fill="#31C5B1" opacity="1.000000" stroke="none" + d=" +M456.722168,42.940254 + C457.029297,50.506069 456.991394,58.325230 456.912781,66.575485 + C456.267456,65.588051 455.105225,64.150276 455.153412,62.754272 + C455.378571,56.227737 455.937469,49.712730 456.722168,42.940254 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M194.731934,165.100403 + C191.957565,169.131729 188.851440,173.245544 185.454941,177.380219 + C188.243088,173.328354 191.321625,169.255615 194.731934,165.100403 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M241.034378,943.943115 + C236.364349,941.944641 231.784271,939.837036 227.207275,937.247192 + C227.210373,936.764832 227.489731,936.456482 227.820953,936.574585 + C232.535446,938.999817 236.918732,941.306946 241.302032,943.614014 + C241.302032,943.614014 241.124313,943.833984 241.034378,943.943115 +z"/> +<path fill="#7F9DA3" opacity="1.000000" stroke="none" + d=" +M565.498962,70.090500 + C565.697205,61.835217 566.269714,53.474617 566.854797,44.657387 + C568.783508,52.882217 569.156189,61.504173 565.498962,70.090500 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M222.393082,934.758179 + C218.668427,932.067688 214.943787,929.377136 211.081543,926.374634 + C210.943954,926.062683 211.282928,925.696411 211.678467,925.610718 + C212.354584,925.720764 212.665527,925.807068 213.145172,925.999390 + C213.684586,926.524109 214.085602,926.833435 214.705933,927.427551 + C217.654785,929.761658 220.384354,931.811035 222.928955,934.081055 + C222.627045,934.453796 222.510071,934.606018 222.393082,934.758179 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M660.805725,112.763794 + C658.286804,112.076759 655.649536,111.048347 652.868408,109.709381 + C655.378784,110.406677 658.033081,111.414543 660.805725,112.763794 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M161.939117,832.409668 + C161.889160,831.966003 162.226593,831.896301 162.398819,831.898621 + C162.847382,834.649475 163.123734,837.398071 163.225769,840.575989 + C162.697311,838.288025 162.343185,835.570740 161.939117,832.409668 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M163.015701,841.388062 + C163.547180,843.140381 164.076660,845.324341 164.330139,847.747437 + C163.707321,845.930847 163.360519,843.875305 163.015701,841.388062 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M224.683655,140.010986 + C223.599106,141.065979 222.158813,142.152878 220.364136,143.150177 + C221.449112,142.054703 222.888519,141.048782 224.683655,140.010986 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M227.070450,936.918762 + C225.548859,936.480225 224.167191,935.887756 222.589325,935.026733 + C222.510071,934.606018 222.627045,934.453796 223.249023,934.136841 + C224.999268,934.800171 226.244507,935.628296 227.489746,936.456421 + C227.489731,936.456482 227.210373,936.764832 227.070450,936.918762 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M219.701996,143.018097 + C218.934677,144.101730 217.813522,145.179688 216.344193,146.149841 + C217.113388,145.032181 218.230759,144.022293 219.701996,143.018097 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M651.792419,108.764847 + C650.374390,108.697617 648.802185,108.297722 647.027466,107.609283 + C648.429382,107.691216 650.033813,108.061699 651.792419,108.764847 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M670.700195,118.123581 + C671.004333,118.285271 671.019348,118.694420 671.027222,118.899185 + C670.028381,118.678749 669.021606,118.253540 667.947815,117.493896 + C668.719177,117.426941 669.557617,117.694420 670.700195,118.123581 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M227.711273,138.013611 + C227.254410,138.707520 226.460587,139.416077 225.319763,140.079605 + C225.773285,139.365799 226.573792,138.697021 227.711273,138.013611 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M645.786377,106.748795 + C644.805908,106.898689 643.636841,106.730606 642.216980,106.304337 + C643.176697,106.174377 644.387207,106.302605 645.786377,106.748795 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M667.772827,116.825638 + C666.892517,116.694481 665.933105,116.254326 664.925537,115.483963 + C665.816101,115.608055 666.754883,116.062347 667.772827,116.825638 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M811.957642,151.649704 + C811.406799,151.481308 810.779724,151.029556 810.111206,150.264404 + C810.673645,150.422791 811.277527,150.894562 811.957642,151.649704 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M809.940186,149.637283 + C809.502563,149.758560 808.958862,149.589859 808.242065,149.189255 + C808.657410,149.087341 809.245667,149.217331 809.940186,149.637283 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M376.637695,114.066193 + C376.448944,114.526497 375.885895,114.907318 374.968872,115.242020 + C375.164398,114.792831 375.713898,114.389771 376.637695,114.066193 +z"/> +<path fill="#4CD298" opacity="1.000000" stroke="none" + d=" +M265.734436,122.203903 + C266.037292,122.210213 266.015411,122.627853 266.012451,122.836990 + C265.780579,123.091919 265.551697,123.137711 264.973267,123.142502 + C264.893005,122.800194 265.162262,122.498901 265.734436,122.203903 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M164.956284,848.705566 + C164.976349,849.000671 164.583221,849.001587 164.386658,849.000122 + C164.141586,848.829895 164.093094,848.661194 164.040100,848.229126 + C164.335815,848.114014 164.636017,848.262268 164.956284,848.705566 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M664.806885,114.837517 + C664.843811,115.120506 664.358093,115.225540 664.358093,115.225540 + C664.358093,115.225540 663.920227,114.990265 663.839233,114.754814 + C664.095459,114.531090 664.432678,114.542809 664.806885,114.837517 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M374.187561,115.149109 + C374.222382,115.590034 373.845886,115.978600 373.052734,116.297516 + C373.016144,115.850822 373.396210,115.473778 374.187561,115.149109 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M662.845642,113.837112 + C662.577942,114.056519 662.257812,113.993759 661.853149,113.687759 + C662.110168,113.481331 662.451660,113.518135 662.845642,113.837112 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M372.250732,116.187820 + C372.296387,116.567116 371.996918,116.890915 371.363922,117.140533 + C371.322144,116.754997 371.613892,116.443657 372.250732,116.187820 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M813.953918,153.702118 + C813.713806,153.841400 813.468811,153.665741 813.172302,153.211761 + C813.431458,152.998047 813.707520,153.149307 813.953918,153.702118 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M812.928528,152.694763 + C812.684631,152.826950 812.441711,152.643311 812.156128,152.173676 + C812.407043,151.995728 812.679077,152.159485 812.928528,152.694763 +z"/> +<path fill="#2E2B2B" opacity="1.000000" stroke="none" + d=" +M708.473145,191.277771 + C708.995605,200.980957 709.287842,210.648895 709.049622,220.303772 + C708.806946,230.141632 700.273621,238.502396 690.369019,239.046799 + C686.712097,239.247787 683.039307,239.196518 679.373718,239.197357 + C571.557556,239.221786 463.741425,239.242065 355.925262,239.252853 + C334.086792,239.255035 328.132141,233.240723 328.263184,211.306168 + C328.300964,204.983322 328.478699,198.661301 328.788635,191.943115 + C429.027130,191.500854 529.069275,191.463501 629.111389,191.401764 + C655.410950,191.385529 681.710510,191.293823 708.473145,191.277771 +z"/> +<path fill="#000100" opacity="1.000000" stroke="none" + d=" +M495.262878,43.353783 + C503.036163,33.744705 518.169128,32.594967 526.017822,41.704769 + C530.302063,46.677364 532.796204,51.917713 531.328369,59.447845 + C529.649231,68.062172 517.515564,76.813034 507.730530,74.933403 + C493.815094,72.260345 486.376251,56.938816 495.262878,43.353783 +z"/> +<path fill="#FFFEFD" opacity="1.000000" stroke="none" + d=" +M280.012543,853.000793 + C274.585938,847.174133 268.606262,841.764709 263.836487,835.442505 + C249.616028,816.593689 243.088242,794.982666 243.061478,771.499329 + C242.955917,678.871582 242.994949,586.243652 242.985275,493.615814 + C242.982635,468.459686 242.903763,443.303101 243.023254,418.147522 + C243.279373,364.230225 280.783264,320.168030 333.963470,311.337677 + C339.193878,310.469238 344.561615,310.063751 349.866089,310.059326 + C457.820831,309.968811 565.775574,309.987640 673.730347,309.923553 + C686.391602,309.916016 698.542175,312.221039 710.956421,317.110474 + C714.208862,318.461761 716.783264,319.606689 719.326721,320.816620 + C732.237610,326.958344 742.848145,336.165771 752.962769,346.053680 + C752.996033,346.032593 752.917358,346.035553 752.991760,346.306854 + C753.349854,346.813904 753.633545,347.049652 753.956970,347.473633 + C754.117798,347.913452 754.315796,348.063599 754.728271,348.381989 + C755.205383,348.844452 755.544861,349.037262 755.866516,349.161255 + C755.848633,349.092407 755.716675,349.145508 755.767639,349.434631 + C756.215515,350.133972 756.612427,350.544189 756.999573,350.978455 + C756.989746,351.002502 757.041687,351.004028 757.097778,351.261414 + C757.417908,351.740967 757.682007,351.963135 758.055969,352.502197 + C772.532837,372.286804 778.836487,394.112030 778.797241,418.299408 + C778.605347,536.619324 778.744690,654.939697 778.686584,773.259949 + C778.683960,778.568298 778.346191,783.955933 777.413147,789.172180 + C768.127808,841.082336 724.888916,878.058533 672.145813,878.212402 + C564.658691,878.525879 457.169952,878.385803 349.682343,878.192505 + C341.100525,878.177063 332.413483,876.854980 323.965576,875.180359 + C308.323730,872.079590 294.769073,864.267273 282.046844,854.919006 + C282.046844,854.919006 282.003235,854.973633 281.885376,854.674683 + C281.179199,854.224792 280.590881,854.073914 280.002563,853.923096 + C280.002563,853.923096 280.005829,853.999634 280.167297,853.738770 + C280.223358,853.318909 280.117950,853.159851 280.012543,853.000793 +M478.595825,743.095276 + C498.663422,722.884033 518.741638,702.683411 538.781982,682.445190 + C540.766357,680.441223 542.776978,678.379089 544.325562,676.045410 + C547.541199,671.199585 547.714111,666.002258 544.815735,660.940613 + C541.907532,655.862061 537.212158,652.817749 531.551514,654.052673 + C527.463562,654.944580 523.108032,657.281982 520.144531,660.246765 + C487.175385,693.229980 454.484741,726.491394 421.647522,759.606934 + C412.187042,769.147583 402.497681,778.461243 392.887909,787.903931 + C381.134491,779.257324 369.358521,770.583130 357.570435,761.925415 + C347.914215,754.833557 338.425537,747.493896 328.527679,740.756042 + C318.782501,734.122070 306.343109,739.964172 305.405609,751.358704 + C304.857025,758.026367 308.485413,762.622070 313.634674,766.348206 + C336.016266,782.544189 358.390045,798.751282 380.711761,815.029541 + C392.577515,823.682678 398.964447,823.247986 409.428772,812.720459 + C432.325043,789.685913 455.210907,766.641052 478.595825,743.095276 +M593.593506,547.724487 + C598.709778,558.279968 607.850586,563.773560 618.538757,566.947998 + C634.203430,571.600586 650.300415,572.348877 666.527588,571.451294 + C678.396240,570.794800 690.086609,569.317322 700.942017,564.045959 + C716.867798,556.312317 724.129761,539.352295 717.625061,524.607910 + C713.404175,515.040283 705.594299,509.210144 695.949158,505.874756 + C675.140747,498.679108 653.758911,498.761261 632.282715,501.611450 + C622.493408,502.910583 613.009094,505.576569 604.713928,511.268127 + C591.945801,520.028687 587.876953,532.894287 593.593506,547.724487 +M430.311920,521.333984 + C424.412872,511.494080 414.973907,506.780029 404.435089,504.045685 + C385.820618,499.216125 366.927155,498.851624 347.966248,501.604187 + C337.681488,503.097290 327.736176,505.745728 319.125580,511.965790 + C300.820587,525.188904 301.548828,549.719604 320.819763,561.451599 + C325.442627,564.265930 330.658661,566.447205 335.888275,567.858521 + C350.641968,571.840271 365.764771,572.477539 380.966400,571.480652 + C392.799683,570.704712 404.512909,569.291504 415.380096,564.060608 + C432.444458,555.846741 438.867523,538.172668 430.311920,521.333984 +M683.201294,745.417236 + C678.875183,745.513611 674.549194,745.691833 670.223022,745.693970 + C622.578979,745.717346 574.934998,745.689270 527.290955,745.732788 + C514.636780,745.744324 506.119385,753.155151 506.035461,763.984863 + C505.949615,775.067200 514.631836,783.022522 527.076965,783.090088 + C542.236023,783.172302 557.395813,783.141663 572.555237,783.141907 + C609.371033,783.142639 646.186951,783.094604 683.002625,783.160950 + C690.032715,783.173584 695.674194,780.708801 699.725586,774.944824 + C707.980652,763.200256 700.328857,746.028748 683.201294,745.417236 +M573.435547,517.796875 + C563.290527,513.203308 554.096924,517.702332 552.040833,528.681213 + C550.224304,538.380920 545.466309,546.021118 537.482544,551.629333 + C513.555176,568.437317 474.801666,556.611938 472.075714,527.791504 + C471.571320,522.458679 468.217896,518.713257 463.056824,516.828613 + C451.910858,512.758362 441.973785,522.234802 444.109802,535.145996 + C445.820068,545.483887 449.907104,554.909119 456.439301,563.146667 + C473.917084,585.187134 505.757721,594.419189 534.178406,585.122131 + C558.985962,577.006897 575.193726,560.573547 580.340332,534.374939 + C581.639832,527.759460 579.713318,522.207764 573.435547,517.796875 +M339.474243,431.979828 + C335.828156,437.661072 331.469757,443.023926 328.746185,449.117493 + C326.267822,454.662567 327.625061,460.599701 331.880798,465.395721 + C338.238922,472.561127 348.371613,472.942932 355.360992,466.202850 + C360.625397,461.126282 365.369873,455.432312 371.034668,450.868103 + C378.804321,444.608063 383.656525,444.462585 391.265015,450.512360 + C396.940247,455.024902 401.674011,460.736115 406.737183,465.999939 + C412.669189,472.167053 420.260956,473.669586 427.099426,469.884247 + C434.161804,465.975098 437.887146,457.635101 435.282349,449.818817 + C433.785217,445.326324 431.594604,440.831604 428.760559,437.055054 + C407.427307,408.627106 366.645386,406.159454 339.474243,431.979828 +M628.154541,452.611237 + C639.186340,444.087555 644.515686,444.122406 654.848511,453.421021 + C659.164978,457.305450 662.849915,461.882324 666.995239,465.968567 + C672.520691,471.415222 679.697083,472.468719 686.240051,469.049805 + C692.859985,465.590668 696.739807,458.505768 695.058716,451.151093 + C694.083984,446.886444 692.195496,442.593872 689.778137,438.934937 + C676.143799,418.297729 649.069153,409.031464 624.522034,416.614532 + C608.181335,421.662506 595.618164,431.562134 588.567383,447.426849 + C584.819092,455.860718 587.804260,464.808380 595.025513,469.367218 + C602.081177,473.821533 609.730530,472.273621 616.356384,465.097839 + C620.080261,461.064819 623.856323,457.079926 628.154541,452.611237 +z"/> +<path fill="#FDA836" opacity="1.000000" stroke="none" + d=" +M282.027588,855.286072 + C294.769073,864.267273 308.323730,872.079590 323.965576,875.180359 + C332.413483,876.854980 341.100525,878.177063 349.682343,878.192505 + C457.169952,878.385803 564.658691,878.525879 672.145813,878.212402 + C724.888916,878.058533 768.127808,841.082336 777.413147,789.172180 + C778.346191,783.955933 778.683960,778.568298 778.686584,773.259949 + C778.744690,654.939697 778.605347,536.619324 778.797241,418.299408 + C778.836487,394.112030 772.532837,372.286804 757.967163,352.262115 + C757.526184,351.471436 757.283936,351.237732 757.041687,351.004028 + C757.041687,351.004028 756.989746,351.002502 756.955811,350.682068 + C756.520142,349.956268 756.118408,349.550903 755.716675,349.145508 + C755.716675,349.145508 755.848633,349.092407 755.727051,348.896423 + C755.267212,348.504425 754.928955,348.308350 754.590698,348.112305 + C754.315796,348.063599 754.117798,347.913452 753.862549,347.213348 + C753.458069,346.521759 753.187683,346.278656 752.917358,346.035553 + C752.917358,346.035553 752.996033,346.032593 752.997681,345.679779 + C754.340759,342.194855 755.682190,339.062775 757.023621,335.930664 + C761.155518,338.950317 765.653015,341.581299 769.352356,345.060852 + C781.588867,356.570251 793.981995,367.966766 805.493042,380.181274 + C823.151123,398.918518 834.529297,421.255829 837.779663,446.849182 + C839.758423,462.430847 839.326233,478.318695 839.997925,494.538177 + C840.000244,590.007019 839.983948,685.006714 839.959473,780.006409 + C839.957275,788.672791 839.903625,797.339233 839.643494,806.797852 + C838.941223,811.062500 838.469360,814.534912 837.971436,818.370667 + C837.888306,819.458557 837.831360,820.183167 837.563110,821.188965 + C836.904236,823.315308 836.456665,825.160400 835.979858,827.381470 + C835.922607,828.512756 835.894592,829.267944 835.624756,830.283691 + C833.067017,836.462341 830.934265,842.460083 828.401245,848.283752 + C820.399475,866.680786 808.950439,882.542114 793.806335,895.831482 + C767.704224,918.736694 737.199402,930.656067 702.717712,930.805969 + C606.480835,931.224121 510.241028,930.971497 414.002380,930.983704 + C388.899506,932.710754 365.429352,927.081238 343.404388,915.154358 + C329.179504,907.451233 317.245636,896.980347 306.761627,884.706421 + C299.851685,876.616882 292.535645,868.875305 285.492432,860.897583 + C284.116425,859.338989 283.158875,857.410828 282.027588,855.286072 +z"/> +<path fill="#FDBD73" opacity="1.000000" stroke="none" + d=" +M413.902679,931.339478 + C510.241028,930.971497 606.480835,931.224121 702.717712,930.805969 + C737.199402,930.656067 767.704224,918.736694 793.806335,895.831482 + C808.950439,882.542114 820.399475,866.680786 828.401245,848.283752 + C830.934265,842.460083 833.067017,836.462341 835.950623,830.211914 + C836.348572,828.921509 836.178833,827.963501 836.009033,827.005432 + C836.456665,825.160400 836.904236,823.315308 837.901245,821.156677 + C838.299622,819.897827 838.148560,818.952576 837.997559,818.007324 + C838.469360,814.534912 838.941223,811.062500 839.632812,807.262207 + C845.286621,809.205322 843.891052,803.736511 845.998352,801.436340 + C846.639954,802.340088 847.554016,803.085510 847.751892,803.987915 + C850.615173,817.046753 854.427124,830.033813 851.141113,843.632019 + C850.874817,844.733948 850.650146,846.188904 851.139221,847.075500 + C854.719177,853.565063 851.441040,859.364441 849.380493,865.190491 + C841.866272,886.435974 829.715393,904.455261 812.617310,919.343201 + C792.922974,936.492004 770.306152,947.103455 744.003479,951.930786 + C741.806152,952.359619 740.384216,952.721252 738.962341,953.082825 + C738.962341,953.082825 738.652832,953.196716 737.993591,953.117432 + C607.222900,953.027039 477.111450,953.015930 347.000000,953.004822 + C345.504486,952.336853 344.001587,951.075256 342.514587,951.093689 + C329.094482,951.260254 315.678070,951.826538 302.258606,951.895813 + C296.328369,951.926453 290.397705,950.806763 284.459656,950.717957 + C279.073853,950.637390 273.580353,948.060608 267.846802,951.220337 + C266.791840,951.243347 266.163361,951.176880 264.981812,950.771851 + C263.776215,950.327087 263.123688,950.220886 262.471191,950.114746 + C262.120941,950.112305 261.770721,950.109863 260.973389,949.785278 + C259.970612,949.351013 259.414948,949.238953 258.859283,949.126953 + C258.859283,949.126953 258.909851,949.181641 258.909607,948.798828 + C257.638184,947.219116 256.554169,945.360046 255.063782,944.938782 + C248.240509,943.010132 241.385727,941.520691 235.749908,936.607056 + C232.829391,934.060791 229.235718,932.111450 225.661499,930.539734 + C222.122910,928.983582 218.226273,928.241699 214.486649,927.142700 + C214.085602,926.833435 213.684586,926.524109 213.126999,925.637329 + C212.488144,921.854431 210.445160,920.842712 207.467438,921.144897 + C207.038300,920.922852 206.609146,920.700745 205.964981,920.041443 + C205.551910,919.346619 205.307281,919.145752 205.016068,919.001587 + C205.016068,919.001587 204.995773,919.004211 204.876251,918.795471 + C204.563873,918.337646 204.328430,918.139038 204.050385,917.991028 + C204.050385,917.991028 203.999710,918.003967 203.992035,917.612488 + C202.313446,915.821899 200.642563,914.422729 198.971649,913.023560 + C198.971634,913.023621 198.997620,912.997559 198.868042,912.803101 + C198.535645,912.344788 198.284836,912.139648 197.986023,911.993164 + C196.704071,910.612366 195.422150,909.231506 193.936340,907.299805 + C193.469131,906.507568 193.205826,906.266235 192.942535,906.024902 + C192.942551,906.024902 192.998520,906.003723 192.870880,905.816528 + C192.550140,905.360535 192.305328,905.152527 192.008835,905.005249 + C192.008835,905.005249 191.996902,905.002441 191.878357,904.800293 + C191.570862,904.351379 191.337585,904.158203 191.059998,904.018555 + C190.718445,903.613770 190.376892,903.208923 189.890594,902.283875 + C189.492950,901.513672 189.240036,901.263672 188.987137,901.013733 + C188.987137,901.013733 189.000000,901.000000 188.892502,900.814087 + C188.620224,900.384644 188.410507,900.189331 188.155853,900.042297 + C188.155853,900.042297 187.925919,899.810669 187.829956,899.354736 + C187.460892,898.608215 187.187790,898.317566 186.914673,898.026978 + C186.914673,898.026978 186.983658,898.020691 186.878143,897.845154 + C186.609207,897.404358 186.393066,897.192993 186.124237,897.035461 + C186.124237,897.035461 185.883682,896.825439 185.903168,896.223083 + C186.232300,891.567139 185.097229,888.573730 180.447357,888.159607 + C180.150436,887.685425 179.853500,887.211243 179.497314,886.461426 + C182.235214,885.558777 182.196503,884.000732 181.171188,881.728210 + C175.235184,868.571350 170.493652,854.990051 168.032440,840.747681 + C167.309845,836.566223 167.921219,832.154236 168.344193,827.855835 + C173.810349,837.184875 178.628326,846.496338 178.817535,857.515076 + C178.861938,860.101013 180.663101,862.697388 181.818619,865.214294 + C183.672867,869.253052 185.246872,873.508179 187.697311,877.163818 + C190.979248,882.060059 192.335907,888.036987 197.316742,892.062683 + C201.842072,895.720215 205.226151,900.755737 209.550766,904.704773 + C214.256332,909.001465 219.204956,913.144165 224.521591,916.631348 + C231.271408,921.058594 238.599548,924.593079 245.492767,928.815491 + C253.458191,933.694641 262.104797,936.022888 271.281799,937.137634 + C273.935120,937.459961 276.570862,938.974487 278.927979,940.412354 + C281.634979,942.063660 283.610229,942.083435 285.914490,939.670166 + C290.664795,934.695129 296.836243,935.123596 303.045990,935.486938 + C313.717834,936.111389 324.405731,936.462463 335.088043,936.900391 + C335.562317,936.919800 336.331818,936.811401 336.486237,936.514343 + C339.497589,930.720032 345.875610,932.306213 350.923737,930.135132 + C350.009216,927.922302 349.180267,925.916565 347.977051,923.005249 + C349.671295,923.804443 350.536652,924.099854 351.280640,924.582336 + C355.466064,927.296631 359.659149,929.066101 364.994995,929.370667 + C369.887848,929.649963 374.882202,932.078674 379.376343,934.430664 + C385.126617,937.440063 390.791687,937.467468 396.761139,935.972961 + C402.442688,934.550720 408.122437,933.121399 413.902679,931.339478 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M756.843567,335.649292 + C755.682190,339.062775 754.340759,342.194855 752.964478,345.700867 + C742.848145,336.165771 732.237610,326.958344 719.326721,320.816620 + C716.783264,319.606689 714.208862,318.461761 711.331421,317.127136 + C712.242615,316.778168 713.554565,316.208710 714.687195,316.457092 + C729.995605,319.814392 743.908691,326.317169 756.843567,335.649292 +z"/> +<path fill="#FDA836" opacity="1.000000" stroke="none" + d=" +M280.186279,854.166626 + C280.590881,854.073914 281.179199,854.224792 281.920135,854.671204 + C281.505157,854.781189 280.937561,854.595703 280.186279,854.166626 +z"/> +<path fill="#FDA836" opacity="1.000000" stroke="none" + d=" +M279.898254,853.120605 + C280.117950,853.159851 280.223358,853.318909 280.205566,853.736877 + C279.704010,853.814636 279.604553,853.562805 279.898254,853.120605 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M856.997559,550.000000 + C856.997559,605.190002 856.997559,659.880066 856.997559,714.570068 + C856.742004,714.569580 856.486450,714.569092 856.230896,714.568542 + C856.230896,646.226257 856.230896,577.883972 856.230896,509.541626 + C856.486450,509.538391 856.742004,509.535126 856.997559,509.531891 + C856.997559,522.854614 856.997559,536.177307 856.997559,550.000000 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M855.997559,445.000549 + C855.997559,438.547302 855.997559,432.593872 855.997559,426.640411 + C856.213379,426.640930 856.429199,426.641449 856.645020,426.641968 + C856.645020,446.943817 856.645020,467.245667 856.645020,487.547516 + C856.429199,487.550537 856.213379,487.553558 855.997559,487.556549 + C855.997559,473.537842 855.997559,459.519104 855.997559,445.000549 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M855.997559,808.555298 + C855.997559,790.214783 855.997559,772.313599 855.997559,754.412415 + C856.243958,754.413391 856.490356,754.414307 856.736755,754.415222 + C856.736755,773.754272 856.736755,793.093262 856.736755,812.432251 + C856.578003,812.446655 856.419250,812.461060 856.260498,812.475464 + C856.172852,811.315247 856.085205,810.154968 855.997559,808.555298 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M844.997559,602.161743 + C845.117126,600.460510 845.236633,599.185974 845.356201,597.911438 + C845.684021,597.916809 846.011902,597.922180 846.339722,597.927551 + C846.339722,601.296143 846.339722,604.664795 846.339722,608.033386 + C845.892334,608.058594 845.444946,608.083740 844.997559,608.108948 + C844.997559,606.268738 844.997559,604.428589 844.997559,602.161743 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M856.018616,420.559082 + C856.052856,419.664551 856.098877,419.054138 856.144897,418.443726 + C856.343811,418.444092 856.542786,418.444489 856.741760,418.444885 + C856.741760,419.466217 856.747070,420.487610 856.727539,421.508575 + C856.726440,421.564087 856.518494,421.615692 856.406860,421.669098 + C856.281372,421.393799 856.155884,421.118500 856.018616,420.559082 +z"/> +<path fill="#EFC381" opacity="1.000000" stroke="none" + d=" +M739.385437,953.109375 + C740.384216,952.721252 741.806152,952.359619 743.615234,951.997803 + C744.002441,951.997559 744.002747,952.344543 744.004517,952.517944 + C742.607056,952.839539 741.207764,952.987732 739.385437,953.109375 +z"/> +<path fill="#EFC381" opacity="1.000000" stroke="none" + d=" +M346.796021,953.139404 + C477.111450,953.015930 607.222900,953.027039 737.834778,953.072876 + C732.557312,953.471008 726.779480,954.148071 721.001465,954.150757 + C630.607910,954.192749 540.214294,954.139587 449.820709,954.126892 + C415.527924,954.122131 381.235168,954.164368 346.479370,954.119751 + C346.143494,953.746460 346.335419,953.486328 346.796021,953.139404 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M180.564713,888.501587 + C185.097229,888.573730 186.232300,891.567139 185.890808,896.063843 + C184.133316,893.952515 182.407700,891.398071 180.564713,888.501587 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M207.658966,921.424744 + C210.445160,920.842712 212.488144,921.854431 212.988617,925.421936 + C212.665527,925.807068 212.354584,925.720764 211.848190,925.427856 + C210.365097,924.121948 209.107803,922.913208 207.658966,921.424744 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M199.109039,913.318115 + C200.642563,914.422729 202.313446,915.821899 204.001526,917.588135 + C202.427948,916.507690 200.837189,915.060181 199.109039,913.318115 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M205.102859,919.234253 + C205.307281,919.145752 205.551910,919.346619 205.872284,919.809692 + C205.652847,919.940186 205.384537,919.757385 205.102859,919.234253 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M204.131653,918.220581 + C204.328430,918.139038 204.563873,918.337646 204.877426,918.785278 + C204.654968,918.925659 204.393250,918.747742 204.131653,918.220581 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M198.065399,912.238525 + C198.284836,912.139648 198.535645,912.344788 198.861755,912.819946 + C198.704941,912.848755 198.424850,912.666321 198.065399,912.238525 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M193.026886,906.286377 + C193.205826,906.266235 193.469131,906.507568 193.834290,907.001099 + C193.661179,907.018188 193.386230,906.783020 193.026886,906.286377 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M186.173950,897.272583 + C186.393066,897.192993 186.609207,897.404358 186.868210,897.878357 + C186.614944,898.025513 186.368225,897.833008 186.173950,897.272583 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M186.989243,898.323853 + C187.187790,898.317566 187.460892,898.608215 187.825546,899.191650 + C187.632660,899.196533 187.348221,898.908630 186.989243,898.323853 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M188.202271,900.269287 + C188.410507,900.189331 188.620224,900.384644 188.885345,900.820435 + C188.653412,900.964111 188.407745,900.791992 188.202271,900.269287 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M189.072021,901.271606 + C189.240036,901.263672 189.492950,901.513672 189.841278,902.017456 + C189.676758,902.023987 189.416840,901.776672 189.072021,901.271606 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M191.132080,904.248657 + C191.337585,904.158203 191.570862,904.351379 191.877441,904.794434 + C191.655960,904.936646 191.392334,904.765930 191.132080,904.248657 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M192.088135,905.246460 + C192.305328,905.152527 192.550140,905.360535 192.858292,905.843689 + C192.704681,905.867920 192.436050,905.677795 192.088135,905.246460 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M214.705933,927.427490 + C218.226273,928.241699 222.122910,928.983582 225.661499,930.539734 + C229.235718,932.111450 232.829391,934.060791 235.749908,936.607056 + C241.385727,941.520691 248.240509,943.010132 255.063782,944.938782 + C256.554169,945.360046 257.638184,947.219116 258.877930,948.818420 + C253.246033,947.403687 247.645523,945.586548 241.673523,943.691772 + C236.918732,941.306946 232.535446,938.999817 227.820953,936.574585 + C226.244507,935.628296 224.999268,934.800171 223.433960,933.916199 + C220.384354,931.811035 217.654785,929.761658 214.705933,927.427490 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M259.050751,949.351562 + C259.414948,949.238953 259.970612,949.351013 260.737427,949.657593 + C260.379791,949.760193 259.811005,949.668213 259.050751,949.351562 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M262.725647,950.340088 + C263.123688,950.220886 263.776215,950.327087 264.687195,950.638916 + C264.290497,950.751526 263.635315,950.658508 262.725647,950.340088 +z"/> +<path fill="#2E2C2A" opacity="1.000000" stroke="none" + d=" +M478.348419,743.347900 + C455.210907,766.641052 432.325043,789.685913 409.428772,812.720459 + C398.964447,823.247986 392.577515,823.682678 380.711761,815.029541 + C358.390045,798.751282 336.016266,782.544189 313.634674,766.348206 + C308.485413,762.622070 304.857025,758.026367 305.405609,751.358704 + C306.343109,739.964172 318.782501,734.122070 328.527679,740.756042 + C338.425537,747.493896 347.914215,754.833557 357.570435,761.925415 + C369.358521,770.583130 381.134491,779.257324 392.887909,787.903931 + C402.497681,778.461243 412.187042,769.147583 421.647522,759.606934 + C454.484741,726.491394 487.175385,693.229980 520.144531,660.246765 + C523.108032,657.281982 527.463562,654.944580 531.551514,654.052673 + C537.212158,652.817749 541.907532,655.862061 544.815735,660.940613 + C547.714111,666.002258 547.541199,671.199585 544.325562,676.045410 + C542.776978,678.379089 540.766357,680.441223 538.781982,682.445190 + C518.741638,702.683411 498.663422,722.884033 478.348419,743.347900 +z"/> +<path fill="#FA6874" opacity="1.000000" stroke="none" + d=" +M593.420898,547.380493 + C587.876953,532.894287 591.945801,520.028687 604.713928,511.268127 + C613.009094,505.576569 622.493408,502.910583 632.282715,501.611450 + C653.758911,498.761261 675.140747,498.679108 695.949158,505.874756 + C705.594299,509.210144 713.404175,515.040283 717.625061,524.607910 + C724.129761,539.352295 716.867798,556.312317 700.942017,564.045959 + C690.086609,569.317322 678.396240,570.794800 666.527588,571.451294 + C650.300415,572.348877 634.203430,571.600586 618.538757,566.947998 + C607.850586,563.773560 598.709778,558.279968 593.420898,547.380493 +z"/> +<path fill="#FA6874" opacity="1.000000" stroke="none" + d=" +M430.520630,521.645142 + C438.867523,538.172668 432.444458,555.846741 415.380096,564.060608 + C404.512909,569.291504 392.799683,570.704712 380.966400,571.480652 + C365.764771,572.477539 350.641968,571.840271 335.888275,567.858521 + C330.658661,566.447205 325.442627,564.265930 320.819763,561.451599 + C301.548828,549.719604 300.820587,525.188904 319.125580,511.965790 + C327.736176,505.745728 337.681488,503.097290 347.966248,501.604187 + C366.927155,498.851624 385.820618,499.216125 404.435089,504.045685 + C414.973907,506.780029 424.412872,511.494080 430.520630,521.645142 +z"/> +<path fill="#54E6DD" opacity="1.000000" stroke="none" + d=" +M683.619385,745.466919 + C700.328857,746.028748 707.980652,763.200256 699.725586,774.944824 + C695.674194,780.708801 690.032715,783.173584 683.002625,783.160950 + C646.186951,783.094604 609.371033,783.142639 572.555237,783.141907 + C557.395813,783.141663 542.236023,783.172302 527.076965,783.090088 + C514.631836,783.022522 505.949615,775.067200 506.035461,763.984863 + C506.119385,753.155151 514.636780,745.744324 527.290955,745.732788 + C574.934998,745.689270 622.578979,745.717346 670.223022,745.693970 + C674.549194,745.691833 678.875183,745.513611 683.619385,745.466919 +z"/> +<path fill="#2E2C2B" opacity="1.000000" stroke="none" + d=" +M573.774963,517.974915 + C579.713318,522.207764 581.639832,527.759460 580.340332,534.374939 + C575.193726,560.573547 558.985962,577.006897 534.178406,585.122131 + C505.757721,594.419189 473.917084,585.187134 456.439301,563.146667 + C449.907104,554.909119 445.820068,545.483887 444.109802,535.145996 + C441.973785,522.234802 451.910858,512.758362 463.056824,516.828613 + C468.217896,518.713257 471.571320,522.458679 472.075714,527.791504 + C474.801666,556.611938 513.555176,568.437317 537.482544,551.629333 + C545.466309,546.021118 550.224304,538.380920 552.040833,528.681213 + C554.096924,517.702332 563.290527,513.203308 573.774963,517.974915 +z"/> +<path fill="#2E2B2B" opacity="1.000000" stroke="none" + d=" +M339.727905,431.729614 + C366.645386,406.159454 407.427307,408.627106 428.760559,437.055054 + C431.594604,440.831604 433.785217,445.326324 435.282349,449.818817 + C437.887146,457.635101 434.161804,465.975098 427.099426,469.884247 + C420.260956,473.669586 412.669189,472.167053 406.737183,465.999939 + C401.674011,460.736115 396.940247,455.024902 391.265015,450.512360 + C383.656525,444.462585 378.804321,444.608063 371.034668,450.868103 + C365.369873,455.432312 360.625397,461.126282 355.360992,466.202850 + C348.371613,472.942932 338.238922,472.561127 331.880798,465.395721 + C327.625061,460.599701 326.267822,454.662567 328.746185,449.117493 + C331.469757,443.023926 335.828156,437.661072 339.727905,431.729614 +z"/> +<path fill="#2E2C2B" opacity="1.000000" stroke="none" + d=" +M627.881714,452.842285 + C623.856323,457.079926 620.080261,461.064819 616.356384,465.097839 + C609.730530,472.273621 602.081177,473.821533 595.025513,469.367218 + C587.804260,464.808380 584.819092,455.860718 588.567383,447.426849 + C595.618164,431.562134 608.181335,421.662506 624.522034,416.614532 + C649.069153,409.031464 676.143799,418.297729 689.778137,438.934937 + C692.195496,442.593872 694.083984,446.886444 695.058716,451.151093 + C696.739807,458.505768 692.859985,465.590668 686.240051,469.049805 + C679.697083,472.468719 672.520691,471.415222 666.995239,465.968567 + C662.849915,461.882324 659.164978,457.305450 654.848511,453.421021 + C644.515686,444.122406 639.186340,444.087555 627.881714,452.842285 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M755.767639,349.434631 + C756.118408,349.550903 756.520142,349.956268 756.965637,350.658020 + C756.612427,350.544189 756.215515,350.133972 755.767639,349.434631 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M754.728271,348.381989 + C754.928955,348.308350 755.267212,348.504425 755.744934,348.965271 + C755.544861,349.037262 755.205383,348.844452 754.728271,348.381989 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M757.097778,351.261414 + C757.283936,351.237732 757.526184,351.471436 757.857178,351.945251 + C757.682007,351.963135 757.417908,351.740967 757.097778,351.261414 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M752.991760,346.306854 + C753.187683,346.278656 753.458069,346.521759 753.822815,347.025085 + C753.633545,347.049652 753.349854,346.813904 752.991760,346.306854 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M835.979858,827.381470 + C836.178833,827.963501 836.348572,828.921509 836.192383,829.951416 + C835.894592,829.267944 835.922607,828.512756 835.979858,827.381470 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M837.971436,818.370667 + C838.148560,818.952576 838.299622,819.897827 838.112549,820.875366 + C837.831360,820.183167 837.888306,819.458557 837.971436,818.370667 +z"/> +</svg>
\ No newline at end of file diff --git a/src/assets/old-screenshot-my-chore.png b/src/assets/old-screenshot-my-chore.png Binary files differnew file mode 100644 index 0000000..92a3a2d --- /dev/null +++ b/src/assets/old-screenshot-my-chore.png diff --git a/src/assets/screenshot-my-chore.png b/src/assets/screenshot-my-chore.png Binary files differnew file mode 100644 index 0000000..62880cb --- /dev/null +++ b/src/assets/screenshot-my-chore.png diff --git a/src/constants/theme.js b/src/constants/theme.js new file mode 100644 index 0000000..29b60a0 --- /dev/null +++ b/src/constants/theme.js @@ -0,0 +1,6 @@ +import resolveConfig from 'tailwindcss/resolveConfig' +import tailwindConfig from '/tailwind.config.js' + +export const { theme: THEME } = resolveConfig(tailwindConfig) + +export const COLORS = THEME.colors diff --git a/src/contexts/Contexts.jsx b/src/contexts/Contexts.jsx new file mode 100644 index 0000000..1269154 --- /dev/null +++ b/src/contexts/Contexts.jsx @@ -0,0 +1,13 @@ +import QueryContext from './QueryContext' +import RouterContext from './RouterContext' +import ThemeContext from './ThemeContext' + +const Contexts = () => { + const contexts = [ThemeContext, QueryContext, RouterContext] + + return contexts.reduceRight((acc, Context) => { + return <Context>{acc}</Context> + }, {}) +} + +export default Contexts diff --git a/src/contexts/QueryContext.jsx b/src/contexts/QueryContext.jsx new file mode 100644 index 0000000..3087d65 --- /dev/null +++ b/src/contexts/QueryContext.jsx @@ -0,0 +1,11 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const QueryContext = ({ children }) => { + const queryClient = new QueryClient() + + return ( + <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> + ) +} + +export default QueryContext diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx new file mode 100644 index 0000000..6077092 --- /dev/null +++ b/src/contexts/RouterContext.jsx @@ -0,0 +1,116 @@ +import App from '@/App' +import ChoreEdit from '@/views/ChoreEdit/ChoreEdit' +import ChoresOverview from '@/views/ChoresOverview' +import Error from '@/views/Error' +import Settings from '@/views/Settings/Settings' +import { RouterProvider, createBrowserRouter } from 'react-router-dom' +import ForgotPasswordView from '../views/Authorization/ForgotPasswordView' +import LoginView from '../views/Authorization/LoginView' +import SignupView from '../views/Authorization/Signup' +import UpdatePasswordView from '../views/Authorization/UpdatePasswordView' +import MyChores from '../views/Chores/MyChores' +import JoinCircleView from '../views/Circles/JoinCircle' +import ChoreHistory from '../views/History/ChoreHistory' +import Landing from '../views/Landing/Landing' +import PaymentCancelledView from '../views/Payments/PaymentFailView' +import PaymentSuccessView from '../views/Payments/PaymentSuccessView' +import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView' +import TermsView from '../views/Terms/TermsView' +import TestView from '../views/TestView/Test' +import ThingsHistory from '../views/Things/ThingsHistory' +import ThingsView from '../views/Things/ThingsView' +const Router = createBrowserRouter([ + { + path: '/', + element: <App />, + errorElement: <Error />, + children: [ + { + path: '/', + element: <Landing />, + }, + { + path: '/settings', + element: <Settings />, + }, + { + path: '/chores', + element: <ChoresOverview />, + }, + { + path: '/chores/:choreId/edit', + element: <ChoreEdit />, + }, + { + path: '/chores/create', + element: <ChoreEdit />, + }, + { + path: '/chores/:choreId/history', + element: <ChoreHistory />, + }, + { + path: '/my/chores', + element: <MyChores />, + }, + { + path: '/login', + element: <LoginView />, + }, + { + path: '/signup', + element: <SignupView />, + }, + { + path: '/landing', + element: <Landing />, + }, + { + path: '/test', + element: <TestView />, + }, + { + path: '/forgot-password', + element: <ForgotPasswordView />, + }, + { + path: '/password/update', + element: <UpdatePasswordView />, + }, + { + path: '/privacy', + element: <PrivacyPolicyView />, + }, + { + path: '/terms', + element: <TermsView />, + }, + { + path: 'circle/join', + element: <JoinCircleView />, + }, + { + path: 'payments/success', + element: <PaymentSuccessView />, + }, + { + path: 'payments/cancel', + element: <PaymentCancelledView />, + }, + { + path: 'things', + element: <ThingsView />, + }, + { + path: 'things/:id', + element: <ThingsHistory />, + }, + ], + }, +]) + +const RouterContext = ({ children }) => { + return <RouterProvider router={Router} /> +} + +export default RouterContext diff --git a/src/contexts/ThemeContext.jsx b/src/contexts/ThemeContext.jsx new file mode 100644 index 0000000..45e91b5 --- /dev/null +++ b/src/contexts/ThemeContext.jsx @@ -0,0 +1,86 @@ +import { COLORS } from '@/constants/theme' +import { CssBaseline } from '@mui/joy' +import { CssVarsProvider, extendTheme } from '@mui/joy/styles' +import PropType from 'prop-types' + +const primaryColor = 'cyan' + +const shades = [ + '50', + ...Array.from({ length: 9 }, (_, i) => String((i + 1) * 100)), +] + +const getPallete = (key = primaryColor) => { + return shades.reduce((acc, shade) => { + acc[shade] = COLORS[key][shade] + return acc + }, {}) +} + +const primaryPalette = getPallete(primaryColor) + +const theme = extendTheme({ + colorSchemes: { + light: { + palette: { + primary: primaryPalette, + success: { + 50: '#f3faf7', + 100: '#def5eb', + 200: '#b7e7d5', + 300: '#8ed9be', + 400: '#6ecdb0', + 500: '#4ec1a2', + 600: '#46b89a', + 700: '#3cae91', + 800: '#32a487', + 900: '#229d76', + }, + danger: { + 50: '#fef2f2', + 100: '#fde8e8', + 200: '#fbd5d5', + 300: '#f9c1c1', + 400: '#f6a8a8', + 500: '', + 600: '#f47272', + 700: '#e33434', + 800: '#cc1f1a', + 900: '#b91c1c', + }, + }, + warning: { + 50: '#fffdf7', + 100: '#fef8e1', + 200: '#fdecb2', + 300: '#fcd982', + 400: '#fbcf52', + 500: '#f9c222', + 600: '#f6b81e', + 700: '#f3ae1a', + 800: '#f0a416', + 900: '#e99b0e', + }, + }, + }, + dark: { + palette: { + primary: primaryPalette, + }, + }, +}) + +const ThemeContext = ({ children }) => { + return ( + <CssVarsProvider theme={theme}> + <CssBaseline /> + {children} + </CssVarsProvider> + ) +} + +ThemeContext.propTypes = { + children: PropType.node, +} + +export default ThemeContext diff --git a/src/contexts/UserContext.js b/src/contexts/UserContext.js new file mode 100644 index 0000000..7d2527e --- /dev/null +++ b/src/contexts/UserContext.js @@ -0,0 +1,8 @@ +import { createContext } from 'react' + +const UserContext = createContext({ + userProfile: null, + setUserProfile: () => {}, +}) + +export { UserContext } diff --git a/src/hooks/useStickyState.js b/src/hooks/useStickyState.js new file mode 100644 index 0000000..237362a --- /dev/null +++ b/src/hooks/useStickyState.js @@ -0,0 +1,16 @@ +import { useEffect, useState } from 'react' + +const useStickyState = (defaultValue, key) => { + const [value, setValue] = useState(() => { + const stickyValue = window.localStorage.getItem(key) + return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue + }) + + useEffect(() => { + window.localStorage.setItem(key, JSON.stringify(value)) + }, [key, value]) + + return [value, setValue] +} + +export default useStickyState diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..4e6fcdd --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import Contexts from './contexts/Contexts.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + <React.StrictMode> + <Contexts /> + </React.StrictMode>, +) diff --git a/src/manifest/android-chrome-192x192.png b/src/manifest/android-chrome-192x192.png Binary files differnew file mode 100644 index 0000000..2f74125 --- /dev/null +++ b/src/manifest/android-chrome-192x192.png diff --git a/src/manifest/android-chrome-512x512.png b/src/manifest/android-chrome-512x512.png Binary files differnew file mode 100644 index 0000000..86d6620 --- /dev/null +++ b/src/manifest/android-chrome-512x512.png diff --git a/src/manifest/apple-touch-icon.png b/src/manifest/apple-touch-icon.png Binary files differnew file mode 100644 index 0000000..ebdd14f --- /dev/null +++ b/src/manifest/apple-touch-icon.png diff --git a/src/manifest/browserconfig.xml b/src/manifest/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/src/manifest/browserconfig.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<browserconfig> + <msapplication> + <tile> + <square150x150logo src="/mstile-150x150.png"/> + <TileColor>#da532c</TileColor> + </tile> + </msapplication> +</browserconfig> diff --git a/src/manifest/favicon-16x16.png b/src/manifest/favicon-16x16.png Binary files differnew file mode 100644 index 0000000..83d1748 --- /dev/null +++ b/src/manifest/favicon-16x16.png diff --git a/src/manifest/favicon-32x32.png b/src/manifest/favicon-32x32.png Binary files differnew file mode 100644 index 0000000..8d4788b --- /dev/null +++ b/src/manifest/favicon-32x32.png diff --git a/src/manifest/favicon.ico b/src/manifest/favicon.ico Binary files differnew file mode 100644 index 0000000..eb40bda --- /dev/null +++ b/src/manifest/favicon.ico diff --git a/src/manifest/logo.svg b/src/manifest/logo.svg new file mode 100644 index 0000000..f205a47 --- /dev/null +++ b/src/manifest/logo.svg @@ -0,0 +1,1185 @@ +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + width="100%" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve"> + +<path fill="#B5BEFB" opacity="1.000000" stroke="none" + d=" +M457.067200,42.686905 + C459.447144,26.832342 469.528015,16.203293 481.577301,7.055771 + C484.583862,4.773226 488.270081,3.385903 491.321960,1.293556 + C503.687561,1.000000 516.375122,1.000000 529.531372,1.000004 + C532.877991,2.985393 535.601196,5.258084 538.660706,6.906528 + C552.035889,14.112956 561.123352,24.814444 565.647583,39.326729 + C566.080750,40.716061 566.044800,42.251637 566.534424,44.416672 + C566.269714,53.474617 565.697205,61.835217 565.124695,70.195816 + C562.884399,82.024635 556.006897,90.798393 546.797058,98.042542 + C545.892578,98.753990 545.048767,99.542503 543.702881,100.681946 + C542.668823,101.562706 542.108643,102.056770 540.874512,103.145233 + C543.896545,103.378769 546.007507,103.627136 548.124084,103.692657 + C579.398621,104.660835 610.777283,101.454941 641.961609,106.044365 + C641.961609,106.044365 641.997559,106.000000 642.232666,106.281258 + C643.636841,106.730606 644.805908,106.898689 645.975037,107.066772 + C645.975037,107.066772 646.399109,107.196709 646.814575,107.547272 + C648.802185,108.297722 650.374390,108.697617 651.946533,109.097511 + C651.946533,109.097511 652.334167,109.251457 652.673218,109.635696 + C655.649536,111.048347 658.286804,112.076759 660.924011,113.105164 + C660.924011,113.105164 661.338745,113.293716 661.638184,113.612358 + C662.257812,113.993759 662.577942,114.056519 662.898132,114.119278 + C662.898132,114.119278 663.313416,114.351051 663.616821,114.670654 + C663.920227,114.990265 664.358093,115.225540 664.358093,115.225540 + C664.358093,115.225540 664.843811,115.120506 664.843811,115.120506 + C664.843811,115.120506 664.924805,115.072365 664.949219,115.443268 + C665.933105,116.254326 666.892517,116.694481 667.851990,117.134628 + C667.851990,117.134621 667.947266,117.053314 667.981079,117.440826 + C669.021606,118.253540 670.028381,118.678749 671.271301,119.170792 + C671.824585,119.286629 672.140808,119.341209 672.622070,119.696716 + C679.759644,127.731895 687.501282,134.920029 693.530396,143.335159 + C703.686951,157.510941 709.115906,173.618210 708.473145,191.277771 + C681.710510,191.293823 655.410950,191.385529 629.111389,191.401764 + C529.069275,191.463501 429.027130,191.500854 328.965271,191.124725 + C330.406860,167.369614 339.520386,147.529587 356.163544,131.138840 + C359.916351,127.442932 363.727966,123.806702 367.806641,120.045158 + C369.087769,118.981110 370.074799,118.014900 371.061859,117.048691 + C371.061859,117.048691 371.013489,116.999557 371.355469,117.107140 + C371.996918,116.890915 372.296387,116.567116 372.595886,116.243324 + C372.595886,116.243332 372.563263,116.098564 373.016327,116.232864 + C373.845886,115.978600 374.222382,115.590034 374.598877,115.201477 + C374.598877,115.201469 374.529510,115.009590 374.926147,115.148865 + C375.885895,114.907318 376.448944,114.526497 377.012024,114.145676 + C383.126526,111.437981 389.241028,108.730270 396.026550,106.213028 + C424.273346,100.114319 452.383545,104.830887 480.419342,102.608955 + C479.972687,101.906181 479.796143,101.424187 479.455841,101.134781 + C478.702362,100.494049 477.866638,99.950081 477.064423,99.366669 + C472.619659,94.664429 467.385376,90.467857 463.956818,85.114799 + C460.664032,79.973732 459.242096,73.634453 456.975769,66.983047 + C456.991394,58.325230 457.029297,50.506069 457.067200,42.686905 +M495.040314,43.670990 + C486.376251,56.938816 493.815094,72.260345 507.730530,74.933403 + C517.515564,76.813034 529.649231,68.062172 531.328369,59.447845 + C532.796204,51.917713 530.302063,46.677364 526.017822,41.704769 + C518.169128,32.594967 503.036163,33.744705 495.040314,43.670990 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M367.512543,120.143005 + C363.727966,123.806702 359.916351,127.442932 356.163544,131.138840 + C339.520386,147.529587 330.406860,167.369614 328.768921,191.520508 + C328.478699,198.661301 328.300964,204.983322 328.263184,211.306168 + C328.132141,233.240723 334.086792,239.255035 355.925262,239.252853 + C463.741425,239.242065 571.557556,239.221786 679.373718,239.197357 + C683.039307,239.196518 686.712097,239.247787 690.369019,239.046799 + C700.273621,238.502396 708.806946,230.141632 709.049622,220.303772 + C709.287842,210.648895 708.995605,200.980957 708.936157,191.318741 + C709.115906,173.618210 703.686951,157.510941 693.530396,143.335159 + C687.501282,134.920029 679.759644,127.731895 673.147339,119.643036 + C691.453796,119.205421 709.400879,119.116837 727.348022,119.028259 + C743.718384,119.185036 759.522888,122.356789 774.749939,128.267838 + C786.922852,132.993240 797.822937,139.913391 808.099548,148.875977 + C808.958862,149.589859 809.502563,149.758560 810.046326,149.927246 + C810.046326,149.927246 810.021057,149.975540 810.086792,150.276672 + C810.779724,151.029556 811.406799,151.481308 812.033936,151.933075 + C812.033936,151.933060 812.019409,151.965164 812.138550,152.173523 + C812.441711,152.643311 812.684631,152.826950 812.986328,152.932816 + C812.986328,152.932816 813.059509,152.936432 813.168884,153.175323 + C813.468811,153.665741 813.713806,153.841400 814.013184,153.941177 + C814.013184,153.941177 814.051208,153.950363 814.088745,154.311172 + C818.032654,158.893280 821.994446,163.064835 825.836121,167.344177 + C846.403625,190.254852 856.329895,217.527451 856.795593,247.912750 + C857.175781,272.717957 857.808350,297.541016 854.362427,322.368683 + C852.379761,336.653229 853.979675,351.360748 856.036316,365.862640 + C857.653748,377.267303 859.067017,388.958374 853.599670,400.041992 + C852.912598,401.434784 852.977539,403.305450 853.038879,404.941803 + C853.287476,411.573303 853.994141,418.202515 853.938171,424.827576 + C853.815247,439.383301 853.345520,453.936340 852.972229,468.489319 + C852.938599,469.801819 852.610535,471.112061 852.363953,472.412842 + C850.920166,480.029877 848.397461,487.615387 848.193176,495.263733 + C847.398071,525.033203 847.340332,554.822327 846.994141,584.603882 + C846.972412,586.474854 846.830505,588.344421 846.744507,590.214600 + C846.145996,590.232544 845.547485,590.250549 844.948975,590.268494 + C844.658142,587.152283 844.109802,584.033447 844.141357,580.920532 + C844.181458,576.954102 844.996338,572.987915 844.935974,569.029785 + C844.796814,559.899963 844.151367,550.777222 844.039978,541.647644 + C843.875366,528.157349 844.023315,514.663452 843.973206,501.171417 + C843.962769,498.366394 844.243896,495.191406 839.997925,494.538177 + C839.326233,478.318695 839.758423,462.430847 837.779663,446.849182 + C834.529297,421.255829 823.151123,398.918518 805.493042,380.181274 + C793.981995,367.966766 781.588867,356.570251 769.352356,345.060852 + C765.653015,341.581299 761.155518,338.950317 756.843567,335.649292 + C743.908691,326.317169 729.995605,319.814392 714.687195,316.457092 + C713.554565,316.208710 712.242615,316.778168 710.639099,316.950745 + C698.542175,312.221039 686.391602,309.916016 673.730347,309.923553 + C565.775574,309.987640 457.820831,309.968811 349.866089,310.059326 + C344.561615,310.063751 339.193878,310.469238 333.963470,311.337677 + C280.783264,320.168030 243.279373,364.230225 243.023254,418.147522 + C242.903763,443.303101 242.982635,468.459686 242.985275,493.615814 + C242.994949,586.243652 242.955917,678.871582 243.061478,771.499329 + C243.088242,794.982666 249.616028,816.593689 263.836487,835.442505 + C268.606262,841.764709 274.585938,847.174133 279.898254,853.120605 + C279.604553,853.562805 279.704010,853.814636 280.044067,853.997742 + C280.005829,853.999634 280.002563,853.923096 280.186279,854.166626 + C280.937561,854.595703 281.505157,854.781189 282.037994,854.970215 + C282.003235,854.973633 282.046844,854.919006 282.027588,855.286072 + C283.158875,857.410828 284.116425,859.338989 285.492432,860.897583 + C292.535645,868.875305 299.851685,876.616882 306.761627,884.706421 + C317.245636,896.980347 329.179504,907.451233 343.404388,915.154358 + C365.429352,927.081238 388.899506,932.710754 413.902679,931.339478 + C408.122437,933.121399 402.442688,934.550720 396.761139,935.972961 + C390.791687,937.467468 385.126617,937.440063 379.376343,934.430664 + C374.882202,932.078674 369.887848,929.649963 364.994995,929.370667 + C359.659149,929.066101 355.466064,927.296631 351.280640,924.582336 + C350.536652,924.099854 349.671295,923.804443 347.977051,923.005249 + C349.180267,925.916565 350.009216,927.922302 350.923737,930.135132 + C345.875610,932.306213 339.497589,930.720032 336.486237,936.514343 + C336.331818,936.811401 335.562317,936.919800 335.088043,936.900391 + C324.405731,936.462463 313.717834,936.111389 303.045990,935.486938 + C296.836243,935.123596 290.664795,934.695129 285.914490,939.670166 + C283.610229,942.083435 281.634979,942.063660 278.927979,940.412354 + C276.570862,938.974487 273.935120,937.459961 271.281799,937.137634 + C262.104797,936.022888 253.458191,933.694641 245.492767,928.815491 + C238.599548,924.593079 231.271408,921.058594 224.521591,916.631348 + C219.204956,913.144165 214.256332,909.001465 209.550766,904.704773 + C205.226151,900.755737 201.842072,895.720215 197.316742,892.062683 + C192.335907,888.036987 190.979248,882.060059 187.697311,877.163818 + C185.246872,873.508179 183.672867,869.253052 181.818619,865.214294 + C180.663101,862.697388 178.861938,860.101013 178.817535,857.515076 + C178.628326,846.496338 173.810349,837.184875 168.344193,827.855835 + C167.921219,832.154236 167.309845,836.566223 168.032440,840.747681 + C170.493652,854.990051 175.235184,868.571350 181.171188,881.728210 + C182.196503,884.000732 182.235214,885.558777 179.185074,885.971924 + C174.280182,873.505493 169.628265,861.253113 164.956284,848.705566 + C164.636017,848.262268 164.335815,848.114014 164.035629,847.965759 + C164.035629,847.965759 164.018539,847.998840 164.312347,847.753601 + C164.076660,845.324341 163.547180,843.140381 163.017685,840.956360 + C163.017685,840.956360 163.043472,840.974731 163.221771,840.560669 + C163.123734,837.398071 162.847382,834.649475 162.610870,831.475159 + C162.540268,823.223755 162.330643,815.398010 162.334015,807.572449 + C162.415344,619.038269 162.519424,430.504120 162.764160,241.573059 + C164.396896,224.044144 169.042175,207.829910 176.951843,192.546234 + C177.533127,191.422989 177.651611,190.060211 177.985229,188.808792 + C180.320938,185.255600 182.656647,181.702408 185.368835,177.754288 + C188.851440,173.245544 191.957565,169.131729 195.063690,165.017914 + C195.063675,165.017914 195.003845,165.003922 195.364883,165.080093 + C199.946686,161.225510 204.181549,157.309738 208.382980,153.358398 + C210.961136,150.933762 213.488052,148.454681 216.037979,146.000031 + C216.037979,146.000031 216.029800,146.033829 216.361084,146.145721 + C217.813522,145.179688 218.934677,144.101730 220.055832,143.023773 + C220.055832,143.023773 220.000656,143.001984 220.359589,143.120880 + C222.158813,142.152878 223.599106,141.065979 225.039383,139.979095 + C225.039383,139.979095 225.032379,140.038437 225.349579,140.081543 + C226.460587,139.416077 227.254410,138.707520 228.048218,137.998962 + C228.048218,137.998962 228.037643,138.048889 228.387024,138.114716 + C233.485779,135.763092 238.235138,133.345642 243.359009,130.899567 + C250.604446,128.297592 257.475372,125.724258 264.346283,123.150917 + C264.346283,123.150917 264.479218,123.093704 264.901001,123.138603 + C265.551697,123.137711 265.780579,123.091919 266.430298,123.052216 + C274.237335,121.799263 281.623535,120.540222 289.476929,119.317017 + C314.376343,119.306656 338.808563,119.244797 363.240784,119.254318 + C364.664825,119.254875 366.088623,119.833435 367.512543,120.143005 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M840.018799,495.007324 + C844.243896,495.191406 843.962769,498.366394 843.973206,501.171417 + C844.023315,514.663452 843.875366,528.157349 844.039978,541.647644 + C844.151367,550.777222 844.796814,559.899963 844.935974,569.029785 + C844.996338,572.987915 844.181458,576.954102 844.141357,580.920532 + C844.109802,584.033447 844.658142,587.152283 844.948975,590.268494 + C845.547485,590.250549 846.145996,590.232544 846.744507,590.214600 + C846.830505,588.344421 846.972412,586.474854 846.994141,584.603882 + C847.340332,554.822327 847.398071,525.033203 848.193176,495.263733 + C848.397461,487.615387 850.920166,480.029877 852.363953,472.412842 + C852.610535,471.112061 852.938599,469.801819 852.972229,468.489319 + C853.345520,453.936340 853.815247,439.383301 853.938171,424.827576 + C853.994141,418.202515 853.287476,411.573303 853.038879,404.941803 + C852.977539,403.305450 852.912598,401.434784 853.599670,400.041992 + C859.067017,388.958374 857.653748,377.267303 856.036316,365.862640 + C853.979675,351.360748 852.379761,336.653229 854.362427,322.368683 + C857.808350,297.541016 857.175781,272.717957 856.795593,247.912750 + C856.329895,217.527451 846.403625,190.254852 825.836121,167.344177 + C821.994446,163.064835 818.032654,158.893280 814.095337,154.329620 + C820.264893,160.224182 826.899231,166.093796 832.564697,172.783936 + C841.113281,182.878738 846.997009,194.774704 851.359863,207.113754 + C854.489685,215.965591 855.696167,225.527374 857.407471,234.837357 + C858.177551,239.026825 858.343750,243.374557 858.346741,247.650787 + C858.401428,326.463135 858.385925,405.275482 858.399048,484.087860 + C858.400269,491.249817 858.513855,498.411652 858.543823,505.573700 + C858.844360,577.417786 859.186035,649.261780 859.413391,721.106079 + C859.523010,755.761902 859.915344,790.429138 859.178833,825.069885 + C858.842896,840.865723 855.393066,856.446045 849.288391,871.254150 + C839.527588,894.930969 823.921387,914.099121 803.508240,929.201782 + C795.927734,934.810120 787.249023,939.034180 778.781982,943.318115 + C768.792419,948.372375 758.154053,951.665405 746.162720,952.788208 + C744.932739,952.769287 744.469482,952.730286 744.004517,952.517944 + C744.002747,952.344543 744.002441,951.997559 744.390686,951.930542 + C770.306152,947.103455 792.922974,936.492004 812.617310,919.343201 + C829.715393,904.455261 841.866272,886.435974 849.380493,865.190491 + C851.441040,859.364441 854.719177,853.565063 851.139221,847.075500 + C850.650146,846.188904 850.874817,844.733948 851.141113,843.632019 + C854.427124,830.033813 850.615173,817.046753 847.751892,803.987915 + C847.554016,803.085510 846.639954,802.340088 845.998352,801.436340 + C843.891052,803.736511 845.286621,809.205322 839.863281,806.469971 + C839.903625,797.339233 839.957275,788.672791 839.959473,780.006409 + C839.983948,685.006714 840.000244,590.007019 840.018799,495.007324 +M856.997559,549.500000 + C856.997559,536.177307 856.997559,522.854614 856.997559,509.531891 + C856.742004,509.535126 856.486450,509.538391 856.230896,509.541626 + C856.230896,577.883972 856.230896,646.226257 856.230896,714.568542 + C856.486450,714.569092 856.742004,714.569580 856.997559,714.570068 + C856.997559,659.880066 856.997559,605.190002 856.997559,549.500000 +M855.997559,445.500366 + C855.997559,459.519104 855.997559,473.537842 855.997559,487.556549 + C856.213379,487.553558 856.429199,487.550537 856.645020,487.547516 + C856.645020,467.245667 856.645020,446.943817 856.645020,426.641968 + C856.429199,426.641449 856.213379,426.640930 855.997559,426.640411 + C855.997559,432.593872 855.997559,438.547302 855.997559,445.500366 +M855.997498,808.994690 + C856.085205,810.154968 856.172852,811.315247 856.260498,812.475464 + C856.419250,812.461060 856.578003,812.446655 856.736755,812.432251 + C856.736755,793.093262 856.736755,773.754272 856.736755,754.415222 + C856.490356,754.414307 856.243958,754.413391 855.997559,754.412415 + C855.997559,772.313599 855.997559,790.214783 855.997498,808.994690 +M844.997559,602.588379 + C844.997559,604.428589 844.997559,606.268738 844.997559,608.108948 + C845.444946,608.083740 845.892334,608.058594 846.339722,608.033386 + C846.339722,604.664795 846.339722,601.296143 846.339722,597.927551 + C846.011902,597.922180 845.684021,597.916809 845.356201,597.911438 + C845.236633,599.185974 845.117126,600.460510 844.997559,602.588379 +M856.030396,420.843170 + C856.155884,421.118500 856.281372,421.393799 856.406860,421.669098 + C856.518494,421.615692 856.726440,421.564087 856.727539,421.508575 + C856.747070,420.487610 856.741760,419.466217 856.741760,418.444885 + C856.542786,418.444489 856.343811,418.444092 856.144897,418.443726 + C856.098877,419.054138 856.052856,419.664551 856.030396,420.843170 +z"/> +<path fill="#637E63" opacity="1.000000" stroke="none" + d=" +M744.006287,952.691284 + C744.469482,952.730286 744.932739,952.769287 745.739624,952.877441 + C741.887573,953.948425 737.692993,955.819275 733.495789,955.825012 + C622.607300,955.977966 511.718536,955.925293 400.829803,955.938599 + C362.204681,955.943237 323.579346,956.101257 284.954834,955.969238 + C280.997284,955.955750 277.044922,954.428162 273.326263,953.260864 + C297.713715,953.297180 321.865051,953.675781 346.479370,954.119751 + C381.235168,954.164368 415.527924,954.122131 449.820709,954.126892 + C540.214294,954.139587 630.607910,954.192749 721.001465,954.150757 + C726.779480,954.148071 732.557312,953.471008 738.494019,953.152161 + C738.652832,953.196716 738.962341,953.082825 739.385437,953.109375 + C741.207764,952.987732 742.607056,952.839539 744.006287,952.691284 +z"/> +<path fill="#06b6d4" opacity="1.000000" stroke="none" + d=" +M162.619415,241.969971 + C162.519424,430.504120 162.415344,619.038269 162.334015,807.572449 + C162.330643,815.398010 162.540268,823.223755 162.438629,831.472839 + C162.226593,831.896301 161.889160,831.966003 161.889160,831.966003 + C161.592010,826.450500 161.246964,820.936951 161.017670,815.418579 + C160.900940,812.609009 160.992310,809.790710 160.987473,806.040527 + C160.990158,722.842468 161.000259,640.580322 160.964508,558.318115 + C160.963745,556.565186 160.435410,554.812500 160.153198,553.059692 + C160.112396,550.988770 160.071609,548.917847 160.337311,546.068665 + C160.760559,544.471375 160.978836,543.652466 160.979370,542.833374 + C160.997757,514.500916 161.006149,486.168457 160.961838,457.836121 + C160.959839,456.563782 160.453461,455.292236 160.182007,454.020325 + C160.112488,423.247040 160.009888,392.473785 159.981750,361.700470 + C159.945847,322.438934 159.951889,283.177307 160.025192,243.915878 + C160.028671,242.056046 160.859131,240.197739 161.656052,238.478958 + C162.211578,239.736115 162.415497,240.853043 162.619415,241.969971 +z"/> +<path fill="#00FFFF" opacity="1.000000" stroke="none" + d=" +M160.084442,553.520630 + C160.435410,554.812500 160.963745,556.565186 160.964508,558.318115 + C161.000259,640.580322 160.990158,722.842468 160.966766,805.575073 + C160.632248,804.539062 160.040527,803.032654 160.039810,801.526062 + C160.000397,719.011230 160.010559,636.496460 160.084442,553.520630 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M346.016357,954.054382 + C321.865051,953.675781 297.713715,953.297180 273.080811,952.937500 + C272.599182,952.956421 272.260376,952.915649 271.814270,952.691528 + C270.052246,952.081604 268.736267,951.695740 267.846802,951.220337 + C273.580353,948.060608 279.073853,950.637390 284.459656,950.717957 + C290.397705,950.806763 296.328369,951.926453 302.258606,951.895813 + C315.678070,951.826538 329.094482,951.260254 342.514587,951.093689 + C344.001587,951.075256 345.504486,952.336853 346.796021,953.139404 + C346.335419,953.486328 346.143494,953.746460 346.016357,954.054382 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M641.746216,105.740341 + C610.777283,101.454941 579.398621,104.660835 548.124084,103.692657 + C546.007507,103.627136 543.896545,103.378769 540.874512,103.145233 + C542.108643,102.056770 542.668823,101.562706 543.529297,100.978439 + C545.653198,101.292381 547.475647,102.042229 549.300537,102.048439 + C573.962708,102.132462 598.628418,101.899818 623.285339,102.270638 + C629.381653,102.362328 635.449768,104.330650 641.746216,105.740341 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M164.976349,849.000671 + C169.628265,861.253113 174.280182,873.505493 179.244354,886.247498 + C179.853500,887.211243 180.150436,887.685425 180.564713,888.501587 + C182.407700,891.398071 184.133316,893.952515 185.871307,896.666260 + C185.883682,896.825439 186.124237,897.035461 186.173950,897.272583 + C186.368225,897.833008 186.614944,898.025513 186.973724,898.053894 + C186.983658,898.020691 186.914673,898.026978 186.989227,898.323853 + C187.348221,898.908630 187.632660,899.196533 187.921509,899.647583 + C187.925919,899.810669 188.155853,900.042297 188.202271,900.269287 + C188.407745,900.791992 188.653412,900.964111 188.992844,901.006348 + C189.000000,901.000000 188.987137,901.013733 189.072021,901.271606 + C189.416840,901.776672 189.676758,902.023987 189.986023,902.537720 + C190.376892,903.208923 190.718445,903.613770 191.132080,904.248657 + C191.392334,904.765930 191.655960,904.936646 191.995972,904.996582 + C191.996902,905.002441 192.008835,905.005249 192.088135,905.246460 + C192.436050,905.677795 192.704681,905.867920 192.985931,906.030884 + C192.998520,906.003723 192.942551,906.024902 193.026917,906.286377 + C193.386230,906.783020 193.661179,907.018188 194.038177,907.552002 + C195.422150,909.231506 196.704071,910.612366 198.065399,912.238525 + C198.424850,912.666321 198.704941,912.848755 198.991333,913.014404 + C198.997620,912.997559 198.971634,913.023621 199.109039,913.318115 + C200.837189,915.060181 202.427948,916.507690 204.009216,917.979614 + C203.999710,918.003967 204.050385,917.991028 204.131653,918.220581 + C204.393250,918.747742 204.654968,918.925659 204.996948,918.994019 + C204.995773,919.004211 205.016068,919.001587 205.102859,919.234253 + C205.384537,919.757385 205.652847,919.940186 206.087311,920.246948 + C206.609146,920.700745 207.038300,920.922852 207.658997,921.424683 + C209.107803,922.913208 210.365097,924.121948 211.452667,925.513550 + C211.282928,925.696411 210.943954,926.062683 210.943939,926.062683 + C206.682892,922.596985 201.948410,919.565918 198.273193,915.562439 + C192.928619,909.740662 187.658661,903.631897 183.609131,896.893005 + C178.115723,887.751404 173.389374,878.078918 169.146606,868.282227 + C166.637726,862.489136 165.696335,856.017273 164.316650,849.426147 + C164.583221,849.001587 164.976349,849.000671 164.976349,849.000671 +z"/> +<path fill="#2CC4AC" opacity="1.000000" stroke="none" + d=" +M477.001221,99.704071 + C477.866638,99.950081 478.702362,100.494049 479.455841,101.134781 + C479.796143,101.424187 479.972687,101.906181 480.419342,102.608955 + C452.383545,104.830887 424.273346,100.114319 396.427979,106.251633 + C398.961578,105.079697 401.693359,103.568764 404.583801,103.148270 + C409.605164,102.417793 414.722748,102.103699 419.803162,102.068298 + C437.947723,101.941887 456.093781,102.035713 474.238251,101.903366 + C475.076782,101.897263 478.206116,103.684158 477.001221,99.704071 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M162.764160,241.573059 + C162.415497,240.853043 162.211578,239.736115 162.000977,238.265656 + C163.086914,225.607986 164.658722,213.449402 170.869873,202.393097 + C173.269608,198.121384 175.208389,193.590683 177.671204,188.993378 + C177.651611,190.060211 177.533127,191.422989 176.951843,192.546234 + C169.042175,207.829910 164.396896,224.044144 162.764160,241.573059 +z"/> +<path fill="#00FF99" opacity="1.000000" stroke="none" + d=" +M160.113678,454.484558 + C160.453461,455.292236 160.959839,456.563782 160.961838,457.836121 + C161.006149,486.168457 160.997757,514.500916 160.979370,542.833374 + C160.978836,543.652466 160.760559,544.471375 160.412903,545.630859 + C160.136429,515.630432 160.090897,485.289642 160.113678,454.484558 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M367.806641,120.045166 + C366.088623,119.833435 364.664825,119.254875 363.240784,119.254318 + C338.808563,119.244797 314.376343,119.306656 289.466949,119.043747 + C288.965912,118.554092 288.942047,118.373535 288.918152,118.192978 + C310.462250,118.128761 332.006836,118.127151 353.550110,117.957306 + C359.096130,117.913589 364.637970,117.340462 370.621796,117.029617 + C370.074799,118.014900 369.087769,118.981110 367.806641,120.045166 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M215.668030,145.999268 + C213.488052,148.454681 210.961136,150.933762 208.382980,153.358398 + C204.181549,157.309738 199.946686,161.225510 195.377960,165.107895 + C197.868362,161.645493 200.404190,157.906754 203.626160,154.904373 + C207.189178,151.584167 211.380432,148.938141 215.668030,145.999268 +z"/> +<path fill="#449678" opacity="1.000000" stroke="none" + d=" +M727.067993,118.662300 + C709.400879,119.116837 691.453796,119.205421 672.981384,119.347687 + C672.140808,119.341209 671.824585,119.286629 671.263428,118.966019 + C671.019348,118.694420 671.004333,118.285271 671.004333,118.285271 + C689.598877,118.288956 708.193420,118.292648 727.067993,118.662300 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M267.420319,951.309875 + C268.736267,951.695740 270.052246,952.081604 271.647125,952.657349 + C261.184357,952.121094 250.886978,949.728455 241.210999,944.257202 + C241.124313,943.833984 241.302032,943.614014 241.673523,943.691711 + C247.645523,945.586548 253.246033,947.403687 258.878174,949.201233 + C258.909851,949.181641 258.859283,949.126953 259.050751,949.351562 + C259.811005,949.668213 260.379791,949.760193 261.184509,949.979858 + C261.770721,950.109863 262.120941,950.112305 262.725647,950.340088 + C263.635315,950.658508 264.290497,950.751526 265.240295,950.977417 + C266.163361,951.176880 266.791840,951.243347 267.420319,951.309875 +z"/> +<path fill="#4CD298" opacity="1.000000" stroke="none" + d=" +M263.904022,123.090408 + C257.475372,125.724258 250.604446,128.297592 243.360748,130.718353 + C242.987946,130.565765 242.990158,130.203278 242.990158,130.203278 + C249.814026,127.812149 256.637878,125.421021 263.904022,123.090408 +z"/> +<path fill="#5CCD95" opacity="1.000000" stroke="none" + d=" +M288.503784,118.129204 + C288.942047,118.373535 288.965912,118.554092 288.999756,119.007919 + C281.623535,120.540222 274.237335,121.799263 266.433289,122.843079 + C266.015411,122.627853 266.037292,122.210213 266.037292,122.210220 + C273.388000,120.828621 280.738739,119.447021 288.503784,118.129204 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M242.617218,130.138428 + C242.990158,130.203278 242.987946,130.565765 242.986221,130.746979 + C238.235138,133.345642 233.485779,135.763092 228.361542,138.113708 + C232.739182,135.389130 237.491730,132.731339 242.617218,130.138428 +z"/> +<path fill="#31C5B1" opacity="1.000000" stroke="none" + d=" +M456.722168,42.940254 + C457.029297,50.506069 456.991394,58.325230 456.912781,66.575485 + C456.267456,65.588051 455.105225,64.150276 455.153412,62.754272 + C455.378571,56.227737 455.937469,49.712730 456.722168,42.940254 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M194.731934,165.100403 + C191.957565,169.131729 188.851440,173.245544 185.454941,177.380219 + C188.243088,173.328354 191.321625,169.255615 194.731934,165.100403 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M241.034378,943.943115 + C236.364349,941.944641 231.784271,939.837036 227.207275,937.247192 + C227.210373,936.764832 227.489731,936.456482 227.820953,936.574585 + C232.535446,938.999817 236.918732,941.306946 241.302032,943.614014 + C241.302032,943.614014 241.124313,943.833984 241.034378,943.943115 +z"/> +<path fill="#7F9DA3" opacity="1.000000" stroke="none" + d=" +M565.498962,70.090500 + C565.697205,61.835217 566.269714,53.474617 566.854797,44.657387 + C568.783508,52.882217 569.156189,61.504173 565.498962,70.090500 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M222.393082,934.758179 + C218.668427,932.067688 214.943787,929.377136 211.081543,926.374634 + C210.943954,926.062683 211.282928,925.696411 211.678467,925.610718 + C212.354584,925.720764 212.665527,925.807068 213.145172,925.999390 + C213.684586,926.524109 214.085602,926.833435 214.705933,927.427551 + C217.654785,929.761658 220.384354,931.811035 222.928955,934.081055 + C222.627045,934.453796 222.510071,934.606018 222.393082,934.758179 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M660.805725,112.763794 + C658.286804,112.076759 655.649536,111.048347 652.868408,109.709381 + C655.378784,110.406677 658.033081,111.414543 660.805725,112.763794 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M161.939117,832.409668 + C161.889160,831.966003 162.226593,831.896301 162.398819,831.898621 + C162.847382,834.649475 163.123734,837.398071 163.225769,840.575989 + C162.697311,838.288025 162.343185,835.570740 161.939117,832.409668 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M163.015701,841.388062 + C163.547180,843.140381 164.076660,845.324341 164.330139,847.747437 + C163.707321,845.930847 163.360519,843.875305 163.015701,841.388062 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M224.683655,140.010986 + C223.599106,141.065979 222.158813,142.152878 220.364136,143.150177 + C221.449112,142.054703 222.888519,141.048782 224.683655,140.010986 +z"/> +<path fill="#41C99D" opacity="1.000000" stroke="none" + d=" +M227.070450,936.918762 + C225.548859,936.480225 224.167191,935.887756 222.589325,935.026733 + C222.510071,934.606018 222.627045,934.453796 223.249023,934.136841 + C224.999268,934.800171 226.244507,935.628296 227.489746,936.456421 + C227.489731,936.456482 227.210373,936.764832 227.070450,936.918762 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M219.701996,143.018097 + C218.934677,144.101730 217.813522,145.179688 216.344193,146.149841 + C217.113388,145.032181 218.230759,144.022293 219.701996,143.018097 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M651.792419,108.764847 + C650.374390,108.697617 648.802185,108.297722 647.027466,107.609283 + C648.429382,107.691216 650.033813,108.061699 651.792419,108.764847 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M670.700195,118.123581 + C671.004333,118.285271 671.019348,118.694420 671.027222,118.899185 + C670.028381,118.678749 669.021606,118.253540 667.947815,117.493896 + C668.719177,117.426941 669.557617,117.694420 670.700195,118.123581 +z"/> +<path fill="#48D09A" opacity="1.000000" stroke="none" + d=" +M227.711273,138.013611 + C227.254410,138.707520 226.460587,139.416077 225.319763,140.079605 + C225.773285,139.365799 226.573792,138.697021 227.711273,138.013611 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M645.786377,106.748795 + C644.805908,106.898689 643.636841,106.730606 642.216980,106.304337 + C643.176697,106.174377 644.387207,106.302605 645.786377,106.748795 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M667.772827,116.825638 + C666.892517,116.694481 665.933105,116.254326 664.925537,115.483963 + C665.816101,115.608055 666.754883,116.062347 667.772827,116.825638 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M811.957642,151.649704 + C811.406799,151.481308 810.779724,151.029556 810.111206,150.264404 + C810.673645,150.422791 811.277527,150.894562 811.957642,151.649704 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M809.940186,149.637283 + C809.502563,149.758560 808.958862,149.589859 808.242065,149.189255 + C808.657410,149.087341 809.245667,149.217331 809.940186,149.637283 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M376.637695,114.066193 + C376.448944,114.526497 375.885895,114.907318 374.968872,115.242020 + C375.164398,114.792831 375.713898,114.389771 376.637695,114.066193 +z"/> +<path fill="#4CD298" opacity="1.000000" stroke="none" + d=" +M265.734436,122.203903 + C266.037292,122.210213 266.015411,122.627853 266.012451,122.836990 + C265.780579,123.091919 265.551697,123.137711 264.973267,123.142502 + C264.893005,122.800194 265.162262,122.498901 265.734436,122.203903 +z"/> +<path fill="#47C698" opacity="1.000000" stroke="none" + d=" +M164.956284,848.705566 + C164.976349,849.000671 164.583221,849.001587 164.386658,849.000122 + C164.141586,848.829895 164.093094,848.661194 164.040100,848.229126 + C164.335815,848.114014 164.636017,848.262268 164.956284,848.705566 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M664.806885,114.837517 + C664.843811,115.120506 664.358093,115.225540 664.358093,115.225540 + C664.358093,115.225540 663.920227,114.990265 663.839233,114.754814 + C664.095459,114.531090 664.432678,114.542809 664.806885,114.837517 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M374.187561,115.149109 + C374.222382,115.590034 373.845886,115.978600 373.052734,116.297516 + C373.016144,115.850822 373.396210,115.473778 374.187561,115.149109 +z"/> +<path fill="#5C818F" opacity="1.000000" stroke="none" + d=" +M662.845642,113.837112 + C662.577942,114.056519 662.257812,113.993759 661.853149,113.687759 + C662.110168,113.481331 662.451660,113.518135 662.845642,113.837112 +z"/> +<path fill="#2CC79F" opacity="1.000000" stroke="none" + d=" +M372.250732,116.187820 + C372.296387,116.567116 371.996918,116.890915 371.363922,117.140533 + C371.322144,116.754997 371.613892,116.443657 372.250732,116.187820 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M813.953918,153.702118 + C813.713806,153.841400 813.468811,153.665741 813.172302,153.211761 + C813.431458,152.998047 813.707520,153.149307 813.953918,153.702118 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M812.928528,152.694763 + C812.684631,152.826950 812.441711,152.643311 812.156128,152.173676 + C812.407043,151.995728 812.679077,152.159485 812.928528,152.694763 +z"/> +<path fill="#2E2B2B" opacity="1.000000" stroke="none" + d=" +M708.473145,191.277771 + C708.995605,200.980957 709.287842,210.648895 709.049622,220.303772 + C708.806946,230.141632 700.273621,238.502396 690.369019,239.046799 + C686.712097,239.247787 683.039307,239.196518 679.373718,239.197357 + C571.557556,239.221786 463.741425,239.242065 355.925262,239.252853 + C334.086792,239.255035 328.132141,233.240723 328.263184,211.306168 + C328.300964,204.983322 328.478699,198.661301 328.788635,191.943115 + C429.027130,191.500854 529.069275,191.463501 629.111389,191.401764 + C655.410950,191.385529 681.710510,191.293823 708.473145,191.277771 +z"/> +<path fill="#000100" opacity="1.000000" stroke="none" + d=" +M495.262878,43.353783 + C503.036163,33.744705 518.169128,32.594967 526.017822,41.704769 + C530.302063,46.677364 532.796204,51.917713 531.328369,59.447845 + C529.649231,68.062172 517.515564,76.813034 507.730530,74.933403 + C493.815094,72.260345 486.376251,56.938816 495.262878,43.353783 +z"/> +<path fill="#FFFEFD" opacity="1.000000" stroke="none" + d=" +M280.012543,853.000793 + C274.585938,847.174133 268.606262,841.764709 263.836487,835.442505 + C249.616028,816.593689 243.088242,794.982666 243.061478,771.499329 + C242.955917,678.871582 242.994949,586.243652 242.985275,493.615814 + C242.982635,468.459686 242.903763,443.303101 243.023254,418.147522 + C243.279373,364.230225 280.783264,320.168030 333.963470,311.337677 + C339.193878,310.469238 344.561615,310.063751 349.866089,310.059326 + C457.820831,309.968811 565.775574,309.987640 673.730347,309.923553 + C686.391602,309.916016 698.542175,312.221039 710.956421,317.110474 + C714.208862,318.461761 716.783264,319.606689 719.326721,320.816620 + C732.237610,326.958344 742.848145,336.165771 752.962769,346.053680 + C752.996033,346.032593 752.917358,346.035553 752.991760,346.306854 + C753.349854,346.813904 753.633545,347.049652 753.956970,347.473633 + C754.117798,347.913452 754.315796,348.063599 754.728271,348.381989 + C755.205383,348.844452 755.544861,349.037262 755.866516,349.161255 + C755.848633,349.092407 755.716675,349.145508 755.767639,349.434631 + C756.215515,350.133972 756.612427,350.544189 756.999573,350.978455 + C756.989746,351.002502 757.041687,351.004028 757.097778,351.261414 + C757.417908,351.740967 757.682007,351.963135 758.055969,352.502197 + C772.532837,372.286804 778.836487,394.112030 778.797241,418.299408 + C778.605347,536.619324 778.744690,654.939697 778.686584,773.259949 + C778.683960,778.568298 778.346191,783.955933 777.413147,789.172180 + C768.127808,841.082336 724.888916,878.058533 672.145813,878.212402 + C564.658691,878.525879 457.169952,878.385803 349.682343,878.192505 + C341.100525,878.177063 332.413483,876.854980 323.965576,875.180359 + C308.323730,872.079590 294.769073,864.267273 282.046844,854.919006 + C282.046844,854.919006 282.003235,854.973633 281.885376,854.674683 + C281.179199,854.224792 280.590881,854.073914 280.002563,853.923096 + C280.002563,853.923096 280.005829,853.999634 280.167297,853.738770 + C280.223358,853.318909 280.117950,853.159851 280.012543,853.000793 +M478.595825,743.095276 + C498.663422,722.884033 518.741638,702.683411 538.781982,682.445190 + C540.766357,680.441223 542.776978,678.379089 544.325562,676.045410 + C547.541199,671.199585 547.714111,666.002258 544.815735,660.940613 + C541.907532,655.862061 537.212158,652.817749 531.551514,654.052673 + C527.463562,654.944580 523.108032,657.281982 520.144531,660.246765 + C487.175385,693.229980 454.484741,726.491394 421.647522,759.606934 + C412.187042,769.147583 402.497681,778.461243 392.887909,787.903931 + C381.134491,779.257324 369.358521,770.583130 357.570435,761.925415 + C347.914215,754.833557 338.425537,747.493896 328.527679,740.756042 + C318.782501,734.122070 306.343109,739.964172 305.405609,751.358704 + C304.857025,758.026367 308.485413,762.622070 313.634674,766.348206 + C336.016266,782.544189 358.390045,798.751282 380.711761,815.029541 + C392.577515,823.682678 398.964447,823.247986 409.428772,812.720459 + C432.325043,789.685913 455.210907,766.641052 478.595825,743.095276 +M593.593506,547.724487 + C598.709778,558.279968 607.850586,563.773560 618.538757,566.947998 + C634.203430,571.600586 650.300415,572.348877 666.527588,571.451294 + C678.396240,570.794800 690.086609,569.317322 700.942017,564.045959 + C716.867798,556.312317 724.129761,539.352295 717.625061,524.607910 + C713.404175,515.040283 705.594299,509.210144 695.949158,505.874756 + C675.140747,498.679108 653.758911,498.761261 632.282715,501.611450 + C622.493408,502.910583 613.009094,505.576569 604.713928,511.268127 + C591.945801,520.028687 587.876953,532.894287 593.593506,547.724487 +M430.311920,521.333984 + C424.412872,511.494080 414.973907,506.780029 404.435089,504.045685 + C385.820618,499.216125 366.927155,498.851624 347.966248,501.604187 + C337.681488,503.097290 327.736176,505.745728 319.125580,511.965790 + C300.820587,525.188904 301.548828,549.719604 320.819763,561.451599 + C325.442627,564.265930 330.658661,566.447205 335.888275,567.858521 + C350.641968,571.840271 365.764771,572.477539 380.966400,571.480652 + C392.799683,570.704712 404.512909,569.291504 415.380096,564.060608 + C432.444458,555.846741 438.867523,538.172668 430.311920,521.333984 +M683.201294,745.417236 + C678.875183,745.513611 674.549194,745.691833 670.223022,745.693970 + C622.578979,745.717346 574.934998,745.689270 527.290955,745.732788 + C514.636780,745.744324 506.119385,753.155151 506.035461,763.984863 + C505.949615,775.067200 514.631836,783.022522 527.076965,783.090088 + C542.236023,783.172302 557.395813,783.141663 572.555237,783.141907 + C609.371033,783.142639 646.186951,783.094604 683.002625,783.160950 + C690.032715,783.173584 695.674194,780.708801 699.725586,774.944824 + C707.980652,763.200256 700.328857,746.028748 683.201294,745.417236 +M573.435547,517.796875 + C563.290527,513.203308 554.096924,517.702332 552.040833,528.681213 + C550.224304,538.380920 545.466309,546.021118 537.482544,551.629333 + C513.555176,568.437317 474.801666,556.611938 472.075714,527.791504 + C471.571320,522.458679 468.217896,518.713257 463.056824,516.828613 + C451.910858,512.758362 441.973785,522.234802 444.109802,535.145996 + C445.820068,545.483887 449.907104,554.909119 456.439301,563.146667 + C473.917084,585.187134 505.757721,594.419189 534.178406,585.122131 + C558.985962,577.006897 575.193726,560.573547 580.340332,534.374939 + C581.639832,527.759460 579.713318,522.207764 573.435547,517.796875 +M339.474243,431.979828 + C335.828156,437.661072 331.469757,443.023926 328.746185,449.117493 + C326.267822,454.662567 327.625061,460.599701 331.880798,465.395721 + C338.238922,472.561127 348.371613,472.942932 355.360992,466.202850 + C360.625397,461.126282 365.369873,455.432312 371.034668,450.868103 + C378.804321,444.608063 383.656525,444.462585 391.265015,450.512360 + C396.940247,455.024902 401.674011,460.736115 406.737183,465.999939 + C412.669189,472.167053 420.260956,473.669586 427.099426,469.884247 + C434.161804,465.975098 437.887146,457.635101 435.282349,449.818817 + C433.785217,445.326324 431.594604,440.831604 428.760559,437.055054 + C407.427307,408.627106 366.645386,406.159454 339.474243,431.979828 +M628.154541,452.611237 + C639.186340,444.087555 644.515686,444.122406 654.848511,453.421021 + C659.164978,457.305450 662.849915,461.882324 666.995239,465.968567 + C672.520691,471.415222 679.697083,472.468719 686.240051,469.049805 + C692.859985,465.590668 696.739807,458.505768 695.058716,451.151093 + C694.083984,446.886444 692.195496,442.593872 689.778137,438.934937 + C676.143799,418.297729 649.069153,409.031464 624.522034,416.614532 + C608.181335,421.662506 595.618164,431.562134 588.567383,447.426849 + C584.819092,455.860718 587.804260,464.808380 595.025513,469.367218 + C602.081177,473.821533 609.730530,472.273621 616.356384,465.097839 + C620.080261,461.064819 623.856323,457.079926 628.154541,452.611237 +z"/> +<path fill="#FDA836" opacity="1.000000" stroke="none" + d=" +M282.027588,855.286072 + C294.769073,864.267273 308.323730,872.079590 323.965576,875.180359 + C332.413483,876.854980 341.100525,878.177063 349.682343,878.192505 + C457.169952,878.385803 564.658691,878.525879 672.145813,878.212402 + C724.888916,878.058533 768.127808,841.082336 777.413147,789.172180 + C778.346191,783.955933 778.683960,778.568298 778.686584,773.259949 + C778.744690,654.939697 778.605347,536.619324 778.797241,418.299408 + C778.836487,394.112030 772.532837,372.286804 757.967163,352.262115 + C757.526184,351.471436 757.283936,351.237732 757.041687,351.004028 + C757.041687,351.004028 756.989746,351.002502 756.955811,350.682068 + C756.520142,349.956268 756.118408,349.550903 755.716675,349.145508 + C755.716675,349.145508 755.848633,349.092407 755.727051,348.896423 + C755.267212,348.504425 754.928955,348.308350 754.590698,348.112305 + C754.315796,348.063599 754.117798,347.913452 753.862549,347.213348 + C753.458069,346.521759 753.187683,346.278656 752.917358,346.035553 + C752.917358,346.035553 752.996033,346.032593 752.997681,345.679779 + C754.340759,342.194855 755.682190,339.062775 757.023621,335.930664 + C761.155518,338.950317 765.653015,341.581299 769.352356,345.060852 + C781.588867,356.570251 793.981995,367.966766 805.493042,380.181274 + C823.151123,398.918518 834.529297,421.255829 837.779663,446.849182 + C839.758423,462.430847 839.326233,478.318695 839.997925,494.538177 + C840.000244,590.007019 839.983948,685.006714 839.959473,780.006409 + C839.957275,788.672791 839.903625,797.339233 839.643494,806.797852 + C838.941223,811.062500 838.469360,814.534912 837.971436,818.370667 + C837.888306,819.458557 837.831360,820.183167 837.563110,821.188965 + C836.904236,823.315308 836.456665,825.160400 835.979858,827.381470 + C835.922607,828.512756 835.894592,829.267944 835.624756,830.283691 + C833.067017,836.462341 830.934265,842.460083 828.401245,848.283752 + C820.399475,866.680786 808.950439,882.542114 793.806335,895.831482 + C767.704224,918.736694 737.199402,930.656067 702.717712,930.805969 + C606.480835,931.224121 510.241028,930.971497 414.002380,930.983704 + C388.899506,932.710754 365.429352,927.081238 343.404388,915.154358 + C329.179504,907.451233 317.245636,896.980347 306.761627,884.706421 + C299.851685,876.616882 292.535645,868.875305 285.492432,860.897583 + C284.116425,859.338989 283.158875,857.410828 282.027588,855.286072 +z"/> +<path fill="#FDBD73" opacity="1.000000" stroke="none" + d=" +M413.902679,931.339478 + C510.241028,930.971497 606.480835,931.224121 702.717712,930.805969 + C737.199402,930.656067 767.704224,918.736694 793.806335,895.831482 + C808.950439,882.542114 820.399475,866.680786 828.401245,848.283752 + C830.934265,842.460083 833.067017,836.462341 835.950623,830.211914 + C836.348572,828.921509 836.178833,827.963501 836.009033,827.005432 + C836.456665,825.160400 836.904236,823.315308 837.901245,821.156677 + C838.299622,819.897827 838.148560,818.952576 837.997559,818.007324 + C838.469360,814.534912 838.941223,811.062500 839.632812,807.262207 + C845.286621,809.205322 843.891052,803.736511 845.998352,801.436340 + C846.639954,802.340088 847.554016,803.085510 847.751892,803.987915 + C850.615173,817.046753 854.427124,830.033813 851.141113,843.632019 + C850.874817,844.733948 850.650146,846.188904 851.139221,847.075500 + C854.719177,853.565063 851.441040,859.364441 849.380493,865.190491 + C841.866272,886.435974 829.715393,904.455261 812.617310,919.343201 + C792.922974,936.492004 770.306152,947.103455 744.003479,951.930786 + C741.806152,952.359619 740.384216,952.721252 738.962341,953.082825 + C738.962341,953.082825 738.652832,953.196716 737.993591,953.117432 + C607.222900,953.027039 477.111450,953.015930 347.000000,953.004822 + C345.504486,952.336853 344.001587,951.075256 342.514587,951.093689 + C329.094482,951.260254 315.678070,951.826538 302.258606,951.895813 + C296.328369,951.926453 290.397705,950.806763 284.459656,950.717957 + C279.073853,950.637390 273.580353,948.060608 267.846802,951.220337 + C266.791840,951.243347 266.163361,951.176880 264.981812,950.771851 + C263.776215,950.327087 263.123688,950.220886 262.471191,950.114746 + C262.120941,950.112305 261.770721,950.109863 260.973389,949.785278 + C259.970612,949.351013 259.414948,949.238953 258.859283,949.126953 + C258.859283,949.126953 258.909851,949.181641 258.909607,948.798828 + C257.638184,947.219116 256.554169,945.360046 255.063782,944.938782 + C248.240509,943.010132 241.385727,941.520691 235.749908,936.607056 + C232.829391,934.060791 229.235718,932.111450 225.661499,930.539734 + C222.122910,928.983582 218.226273,928.241699 214.486649,927.142700 + C214.085602,926.833435 213.684586,926.524109 213.126999,925.637329 + C212.488144,921.854431 210.445160,920.842712 207.467438,921.144897 + C207.038300,920.922852 206.609146,920.700745 205.964981,920.041443 + C205.551910,919.346619 205.307281,919.145752 205.016068,919.001587 + C205.016068,919.001587 204.995773,919.004211 204.876251,918.795471 + C204.563873,918.337646 204.328430,918.139038 204.050385,917.991028 + C204.050385,917.991028 203.999710,918.003967 203.992035,917.612488 + C202.313446,915.821899 200.642563,914.422729 198.971649,913.023560 + C198.971634,913.023621 198.997620,912.997559 198.868042,912.803101 + C198.535645,912.344788 198.284836,912.139648 197.986023,911.993164 + C196.704071,910.612366 195.422150,909.231506 193.936340,907.299805 + C193.469131,906.507568 193.205826,906.266235 192.942535,906.024902 + C192.942551,906.024902 192.998520,906.003723 192.870880,905.816528 + C192.550140,905.360535 192.305328,905.152527 192.008835,905.005249 + C192.008835,905.005249 191.996902,905.002441 191.878357,904.800293 + C191.570862,904.351379 191.337585,904.158203 191.059998,904.018555 + C190.718445,903.613770 190.376892,903.208923 189.890594,902.283875 + C189.492950,901.513672 189.240036,901.263672 188.987137,901.013733 + C188.987137,901.013733 189.000000,901.000000 188.892502,900.814087 + C188.620224,900.384644 188.410507,900.189331 188.155853,900.042297 + C188.155853,900.042297 187.925919,899.810669 187.829956,899.354736 + C187.460892,898.608215 187.187790,898.317566 186.914673,898.026978 + C186.914673,898.026978 186.983658,898.020691 186.878143,897.845154 + C186.609207,897.404358 186.393066,897.192993 186.124237,897.035461 + C186.124237,897.035461 185.883682,896.825439 185.903168,896.223083 + C186.232300,891.567139 185.097229,888.573730 180.447357,888.159607 + C180.150436,887.685425 179.853500,887.211243 179.497314,886.461426 + C182.235214,885.558777 182.196503,884.000732 181.171188,881.728210 + C175.235184,868.571350 170.493652,854.990051 168.032440,840.747681 + C167.309845,836.566223 167.921219,832.154236 168.344193,827.855835 + C173.810349,837.184875 178.628326,846.496338 178.817535,857.515076 + C178.861938,860.101013 180.663101,862.697388 181.818619,865.214294 + C183.672867,869.253052 185.246872,873.508179 187.697311,877.163818 + C190.979248,882.060059 192.335907,888.036987 197.316742,892.062683 + C201.842072,895.720215 205.226151,900.755737 209.550766,904.704773 + C214.256332,909.001465 219.204956,913.144165 224.521591,916.631348 + C231.271408,921.058594 238.599548,924.593079 245.492767,928.815491 + C253.458191,933.694641 262.104797,936.022888 271.281799,937.137634 + C273.935120,937.459961 276.570862,938.974487 278.927979,940.412354 + C281.634979,942.063660 283.610229,942.083435 285.914490,939.670166 + C290.664795,934.695129 296.836243,935.123596 303.045990,935.486938 + C313.717834,936.111389 324.405731,936.462463 335.088043,936.900391 + C335.562317,936.919800 336.331818,936.811401 336.486237,936.514343 + C339.497589,930.720032 345.875610,932.306213 350.923737,930.135132 + C350.009216,927.922302 349.180267,925.916565 347.977051,923.005249 + C349.671295,923.804443 350.536652,924.099854 351.280640,924.582336 + C355.466064,927.296631 359.659149,929.066101 364.994995,929.370667 + C369.887848,929.649963 374.882202,932.078674 379.376343,934.430664 + C385.126617,937.440063 390.791687,937.467468 396.761139,935.972961 + C402.442688,934.550720 408.122437,933.121399 413.902679,931.339478 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M756.843567,335.649292 + C755.682190,339.062775 754.340759,342.194855 752.964478,345.700867 + C742.848145,336.165771 732.237610,326.958344 719.326721,320.816620 + C716.783264,319.606689 714.208862,318.461761 711.331421,317.127136 + C712.242615,316.778168 713.554565,316.208710 714.687195,316.457092 + C729.995605,319.814392 743.908691,326.317169 756.843567,335.649292 +z"/> +<path fill="#FDA836" opacity="1.000000" stroke="none" + d=" +M280.186279,854.166626 + C280.590881,854.073914 281.179199,854.224792 281.920135,854.671204 + C281.505157,854.781189 280.937561,854.595703 280.186279,854.166626 +z"/> +<path fill="#FDA836" opacity="1.000000" stroke="none" + d=" +M279.898254,853.120605 + C280.117950,853.159851 280.223358,853.318909 280.205566,853.736877 + C279.704010,853.814636 279.604553,853.562805 279.898254,853.120605 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M856.997559,550.000000 + C856.997559,605.190002 856.997559,659.880066 856.997559,714.570068 + C856.742004,714.569580 856.486450,714.569092 856.230896,714.568542 + C856.230896,646.226257 856.230896,577.883972 856.230896,509.541626 + C856.486450,509.538391 856.742004,509.535126 856.997559,509.531891 + C856.997559,522.854614 856.997559,536.177307 856.997559,550.000000 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M855.997559,445.000549 + C855.997559,438.547302 855.997559,432.593872 855.997559,426.640411 + C856.213379,426.640930 856.429199,426.641449 856.645020,426.641968 + C856.645020,446.943817 856.645020,467.245667 856.645020,487.547516 + C856.429199,487.550537 856.213379,487.553558 855.997559,487.556549 + C855.997559,473.537842 855.997559,459.519104 855.997559,445.000549 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M855.997559,808.555298 + C855.997559,790.214783 855.997559,772.313599 855.997559,754.412415 + C856.243958,754.413391 856.490356,754.414307 856.736755,754.415222 + C856.736755,773.754272 856.736755,793.093262 856.736755,812.432251 + C856.578003,812.446655 856.419250,812.461060 856.260498,812.475464 + C856.172852,811.315247 856.085205,810.154968 855.997559,808.555298 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M844.997559,602.161743 + C845.117126,600.460510 845.236633,599.185974 845.356201,597.911438 + C845.684021,597.916809 846.011902,597.922180 846.339722,597.927551 + C846.339722,601.296143 846.339722,604.664795 846.339722,608.033386 + C845.892334,608.058594 845.444946,608.083740 844.997559,608.108948 + C844.997559,606.268738 844.997559,604.428589 844.997559,602.161743 +z"/> +<path fill="#FFBD76" opacity="1.000000" stroke="none" + d=" +M856.018616,420.559082 + C856.052856,419.664551 856.098877,419.054138 856.144897,418.443726 + C856.343811,418.444092 856.542786,418.444489 856.741760,418.444885 + C856.741760,419.466217 856.747070,420.487610 856.727539,421.508575 + C856.726440,421.564087 856.518494,421.615692 856.406860,421.669098 + C856.281372,421.393799 856.155884,421.118500 856.018616,420.559082 +z"/> +<path fill="#EFC381" opacity="1.000000" stroke="none" + d=" +M739.385437,953.109375 + C740.384216,952.721252 741.806152,952.359619 743.615234,951.997803 + C744.002441,951.997559 744.002747,952.344543 744.004517,952.517944 + C742.607056,952.839539 741.207764,952.987732 739.385437,953.109375 +z"/> +<path fill="#EFC381" opacity="1.000000" stroke="none" + d=" +M346.796021,953.139404 + C477.111450,953.015930 607.222900,953.027039 737.834778,953.072876 + C732.557312,953.471008 726.779480,954.148071 721.001465,954.150757 + C630.607910,954.192749 540.214294,954.139587 449.820709,954.126892 + C415.527924,954.122131 381.235168,954.164368 346.479370,954.119751 + C346.143494,953.746460 346.335419,953.486328 346.796021,953.139404 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M180.564713,888.501587 + C185.097229,888.573730 186.232300,891.567139 185.890808,896.063843 + C184.133316,893.952515 182.407700,891.398071 180.564713,888.501587 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M207.658966,921.424744 + C210.445160,920.842712 212.488144,921.854431 212.988617,925.421936 + C212.665527,925.807068 212.354584,925.720764 211.848190,925.427856 + C210.365097,924.121948 209.107803,922.913208 207.658966,921.424744 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M199.109039,913.318115 + C200.642563,914.422729 202.313446,915.821899 204.001526,917.588135 + C202.427948,916.507690 200.837189,915.060181 199.109039,913.318115 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M205.102859,919.234253 + C205.307281,919.145752 205.551910,919.346619 205.872284,919.809692 + C205.652847,919.940186 205.384537,919.757385 205.102859,919.234253 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M204.131653,918.220581 + C204.328430,918.139038 204.563873,918.337646 204.877426,918.785278 + C204.654968,918.925659 204.393250,918.747742 204.131653,918.220581 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M198.065399,912.238525 + C198.284836,912.139648 198.535645,912.344788 198.861755,912.819946 + C198.704941,912.848755 198.424850,912.666321 198.065399,912.238525 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M193.026886,906.286377 + C193.205826,906.266235 193.469131,906.507568 193.834290,907.001099 + C193.661179,907.018188 193.386230,906.783020 193.026886,906.286377 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M186.173950,897.272583 + C186.393066,897.192993 186.609207,897.404358 186.868210,897.878357 + C186.614944,898.025513 186.368225,897.833008 186.173950,897.272583 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M186.989243,898.323853 + C187.187790,898.317566 187.460892,898.608215 187.825546,899.191650 + C187.632660,899.196533 187.348221,898.908630 186.989243,898.323853 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M188.202271,900.269287 + C188.410507,900.189331 188.620224,900.384644 188.885345,900.820435 + C188.653412,900.964111 188.407745,900.791992 188.202271,900.269287 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M189.072021,901.271606 + C189.240036,901.263672 189.492950,901.513672 189.841278,902.017456 + C189.676758,902.023987 189.416840,901.776672 189.072021,901.271606 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M191.132080,904.248657 + C191.337585,904.158203 191.570862,904.351379 191.877441,904.794434 + C191.655960,904.936646 191.392334,904.765930 191.132080,904.248657 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M192.088135,905.246460 + C192.305328,905.152527 192.550140,905.360535 192.858292,905.843689 + C192.704681,905.867920 192.436050,905.677795 192.088135,905.246460 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M214.705933,927.427490 + C218.226273,928.241699 222.122910,928.983582 225.661499,930.539734 + C229.235718,932.111450 232.829391,934.060791 235.749908,936.607056 + C241.385727,941.520691 248.240509,943.010132 255.063782,944.938782 + C256.554169,945.360046 257.638184,947.219116 258.877930,948.818420 + C253.246033,947.403687 247.645523,945.586548 241.673523,943.691772 + C236.918732,941.306946 232.535446,938.999817 227.820953,936.574585 + C226.244507,935.628296 224.999268,934.800171 223.433960,933.916199 + C220.384354,931.811035 217.654785,929.761658 214.705933,927.427490 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M259.050751,949.351562 + C259.414948,949.238953 259.970612,949.351013 260.737427,949.657593 + C260.379791,949.760193 259.811005,949.668213 259.050751,949.351562 +z"/> +<path fill="#FBBF74" opacity="1.000000" stroke="none" + d=" +M262.725647,950.340088 + C263.123688,950.220886 263.776215,950.327087 264.687195,950.638916 + C264.290497,950.751526 263.635315,950.658508 262.725647,950.340088 +z"/> +<path fill="#2E2C2A" opacity="1.000000" stroke="none" + d=" +M478.348419,743.347900 + C455.210907,766.641052 432.325043,789.685913 409.428772,812.720459 + C398.964447,823.247986 392.577515,823.682678 380.711761,815.029541 + C358.390045,798.751282 336.016266,782.544189 313.634674,766.348206 + C308.485413,762.622070 304.857025,758.026367 305.405609,751.358704 + C306.343109,739.964172 318.782501,734.122070 328.527679,740.756042 + C338.425537,747.493896 347.914215,754.833557 357.570435,761.925415 + C369.358521,770.583130 381.134491,779.257324 392.887909,787.903931 + C402.497681,778.461243 412.187042,769.147583 421.647522,759.606934 + C454.484741,726.491394 487.175385,693.229980 520.144531,660.246765 + C523.108032,657.281982 527.463562,654.944580 531.551514,654.052673 + C537.212158,652.817749 541.907532,655.862061 544.815735,660.940613 + C547.714111,666.002258 547.541199,671.199585 544.325562,676.045410 + C542.776978,678.379089 540.766357,680.441223 538.781982,682.445190 + C518.741638,702.683411 498.663422,722.884033 478.348419,743.347900 +z"/> +<path fill="#FA6874" opacity="1.000000" stroke="none" + d=" +M593.420898,547.380493 + C587.876953,532.894287 591.945801,520.028687 604.713928,511.268127 + C613.009094,505.576569 622.493408,502.910583 632.282715,501.611450 + C653.758911,498.761261 675.140747,498.679108 695.949158,505.874756 + C705.594299,509.210144 713.404175,515.040283 717.625061,524.607910 + C724.129761,539.352295 716.867798,556.312317 700.942017,564.045959 + C690.086609,569.317322 678.396240,570.794800 666.527588,571.451294 + C650.300415,572.348877 634.203430,571.600586 618.538757,566.947998 + C607.850586,563.773560 598.709778,558.279968 593.420898,547.380493 +z"/> +<path fill="#FA6874" opacity="1.000000" stroke="none" + d=" +M430.520630,521.645142 + C438.867523,538.172668 432.444458,555.846741 415.380096,564.060608 + C404.512909,569.291504 392.799683,570.704712 380.966400,571.480652 + C365.764771,572.477539 350.641968,571.840271 335.888275,567.858521 + C330.658661,566.447205 325.442627,564.265930 320.819763,561.451599 + C301.548828,549.719604 300.820587,525.188904 319.125580,511.965790 + C327.736176,505.745728 337.681488,503.097290 347.966248,501.604187 + C366.927155,498.851624 385.820618,499.216125 404.435089,504.045685 + C414.973907,506.780029 424.412872,511.494080 430.520630,521.645142 +z"/> +<path fill="#54E6DD" opacity="1.000000" stroke="none" + d=" +M683.619385,745.466919 + C700.328857,746.028748 707.980652,763.200256 699.725586,774.944824 + C695.674194,780.708801 690.032715,783.173584 683.002625,783.160950 + C646.186951,783.094604 609.371033,783.142639 572.555237,783.141907 + C557.395813,783.141663 542.236023,783.172302 527.076965,783.090088 + C514.631836,783.022522 505.949615,775.067200 506.035461,763.984863 + C506.119385,753.155151 514.636780,745.744324 527.290955,745.732788 + C574.934998,745.689270 622.578979,745.717346 670.223022,745.693970 + C674.549194,745.691833 678.875183,745.513611 683.619385,745.466919 +z"/> +<path fill="#2E2C2B" opacity="1.000000" stroke="none" + d=" +M573.774963,517.974915 + C579.713318,522.207764 581.639832,527.759460 580.340332,534.374939 + C575.193726,560.573547 558.985962,577.006897 534.178406,585.122131 + C505.757721,594.419189 473.917084,585.187134 456.439301,563.146667 + C449.907104,554.909119 445.820068,545.483887 444.109802,535.145996 + C441.973785,522.234802 451.910858,512.758362 463.056824,516.828613 + C468.217896,518.713257 471.571320,522.458679 472.075714,527.791504 + C474.801666,556.611938 513.555176,568.437317 537.482544,551.629333 + C545.466309,546.021118 550.224304,538.380920 552.040833,528.681213 + C554.096924,517.702332 563.290527,513.203308 573.774963,517.974915 +z"/> +<path fill="#2E2B2B" opacity="1.000000" stroke="none" + d=" +M339.727905,431.729614 + C366.645386,406.159454 407.427307,408.627106 428.760559,437.055054 + C431.594604,440.831604 433.785217,445.326324 435.282349,449.818817 + C437.887146,457.635101 434.161804,465.975098 427.099426,469.884247 + C420.260956,473.669586 412.669189,472.167053 406.737183,465.999939 + C401.674011,460.736115 396.940247,455.024902 391.265015,450.512360 + C383.656525,444.462585 378.804321,444.608063 371.034668,450.868103 + C365.369873,455.432312 360.625397,461.126282 355.360992,466.202850 + C348.371613,472.942932 338.238922,472.561127 331.880798,465.395721 + C327.625061,460.599701 326.267822,454.662567 328.746185,449.117493 + C331.469757,443.023926 335.828156,437.661072 339.727905,431.729614 +z"/> +<path fill="#2E2C2B" opacity="1.000000" stroke="none" + d=" +M627.881714,452.842285 + C623.856323,457.079926 620.080261,461.064819 616.356384,465.097839 + C609.730530,472.273621 602.081177,473.821533 595.025513,469.367218 + C587.804260,464.808380 584.819092,455.860718 588.567383,447.426849 + C595.618164,431.562134 608.181335,421.662506 624.522034,416.614532 + C649.069153,409.031464 676.143799,418.297729 689.778137,438.934937 + C692.195496,442.593872 694.083984,446.886444 695.058716,451.151093 + C696.739807,458.505768 692.859985,465.590668 686.240051,469.049805 + C679.697083,472.468719 672.520691,471.415222 666.995239,465.968567 + C662.849915,461.882324 659.164978,457.305450 654.848511,453.421021 + C644.515686,444.122406 639.186340,444.087555 627.881714,452.842285 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M755.767639,349.434631 + C756.118408,349.550903 756.520142,349.956268 756.965637,350.658020 + C756.612427,350.544189 756.215515,350.133972 755.767639,349.434631 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M754.728271,348.381989 + C754.928955,348.308350 755.267212,348.504425 755.744934,348.965271 + C755.544861,349.037262 755.205383,348.844452 754.728271,348.381989 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M757.097778,351.261414 + C757.283936,351.237732 757.526184,351.471436 757.857178,351.945251 + C757.682007,351.963135 757.417908,351.740967 757.097778,351.261414 +z"/> +<path fill="#F7A543" opacity="1.000000" stroke="none" + d=" +M752.991760,346.306854 + C753.187683,346.278656 753.458069,346.521759 753.822815,347.025085 + C753.633545,347.049652 753.349854,346.813904 752.991760,346.306854 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M835.979858,827.381470 + C836.178833,827.963501 836.348572,828.921509 836.192383,829.951416 + C835.894592,829.267944 835.922607,828.512756 835.979858,827.381470 +z"/> +<path fill="#F2BB78" opacity="1.000000" stroke="none" + d=" +M837.971436,818.370667 + C838.148560,818.952576 838.299622,819.897827 838.112549,820.875366 + C837.831360,820.183167 837.888306,819.458557 837.971436,818.370667 +z"/> +</svg>
\ No newline at end of file diff --git a/src/manifest/mstile-150x150.png b/src/manifest/mstile-150x150.png Binary files differnew file mode 100644 index 0000000..6fec85c --- /dev/null +++ b/src/manifest/mstile-150x150.png diff --git a/src/manifest/safari-pinned-tab.svg b/src/manifest/safari-pinned-tab.svg new file mode 100644 index 0000000..e4096f0 --- /dev/null +++ b/src/manifest/safari-pinned-tab.svg @@ -0,0 +1,81 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" + width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000" + preserveAspectRatio="xMidYMid meet"> +<metadata> +Created by potrace 1.14, written by Peter Selinger 2001-2017 +</metadata> +<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)" +fill="#000000" stroke="none"> +<path d="M4925 10226 c-11 -7 -24 -15 -30 -16 -5 -2 -18 -5 -27 -8 -10 -2 -18 +-7 -18 -11 0 -3 -13 -12 -30 -19 -40 -18 -160 -140 -185 -188 -11 -22 -24 -42 +-30 -46 -5 -4 -6 -8 -1 -8 5 0 4 -6 -1 -12 -8 -11 -22 -49 -27 -78 -1 -3 -5 +-11 -9 -17 -4 -7 -10 -50 -14 -95 -5 -83 9 -165 42 -246 8 -18 14 -35 14 -37 +0 -3 10 -15 21 -27 11 -12 20 -28 20 -35 0 -7 3 -13 8 -13 4 0 16 -14 27 -30 +11 -16 23 -30 27 -30 4 0 26 -19 49 -42 41 -41 42 -43 18 -45 -13 -1 -184 -4 +-379 -6 -331 -4 -386 -8 -450 -34 -8 -4 -18 -7 -22 -8 -5 -2 -9 -3 -10 -5 -2 +-1 -11 -3 -20 -5 -32 -6 -168 -77 -168 -88 0 -6 -3 -7 -6 -4 -4 3 -16 1 -27 +-5 -13 -7 -166 -11 -451 -13 -237 -1 -440 -5 -451 -7 -11 -3 -41 -9 -67 -12 +-26 -4 -50 -11 -53 -17 -4 -5 -10 -7 -15 -4 -6 4 -72 -12 -105 -25 -3 -1 -14 +-4 -25 -7 -11 -3 -20 -10 -20 -15 0 -5 -4 -7 -9 -4 -5 4 -30 -3 -55 -14 -26 +-11 -49 -19 -53 -19 -3 0 -9 -3 -12 -8 -3 -4 -31 -20 -61 -35 -70 -35 -93 -50 +-103 -65 -4 -7 -13 -13 -18 -13 -38 0 -261 -210 -330 -310 -13 -19 -27 -37 +-30 -40 -13 -11 -89 -145 -89 -157 0 -7 -4 -13 -10 -13 -5 0 -10 -6 -10 -14 0 +-8 -13 -43 -30 -78 -32 -71 -58 -164 -62 -225 -1 -13 -5 -23 -9 -23 -4 0 -6 +-15 -5 -34 1 -19 -2 -32 -6 -30 -11 7 -11 -1 -13 -773 0 -474 3 -703 10 -703 +7 0 7 -3 0 -8 -14 -10 -16 -112 -1 -112 7 0 7 -3 1 -8 -14 -10 -14 -105 -1 +-114 7 -5 7 -8 1 -8 -11 0 -13 -109 -2 -126 3 -6 4 -14 0 -17 -10 -11 -9 +-1086 1 -1080 5 4 6 0 1 -8 -13 -19 -12 -668 0 -673 7 -3 7 -5 0 -5 -11 -1 +-11 -113 -1 -130 4 -6 4 -11 -1 -11 -10 0 -8 -163 1 -179 4 -6 4 -11 -1 -11 +-9 0 -8 -154 2 -170 3 -5 3 -10 -2 -10 -4 0 -6 -81 -3 -181 3 -99 2 -189 -2 +-199 -4 -11 -4 -21 0 -24 8 -5 6 -80 -2 -102 -2 -6 -2 -15 1 -18 5 -5 5 -35 2 +-151 0 -5 1 -52 2 -103 1 -51 -1 -95 -4 -98 -3 -3 -2 -18 3 -32 4 -15 3 -37 +-1 -50 -5 -14 -5 -22 3 -22 7 0 7 -4 -1 -13 -6 -8 -9 -16 -6 -18 9 -9 12 -79 +3 -79 -5 0 -6 -5 -3 -10 4 -6 7 -101 7 -213 0 -135 4 -207 12 -216 8 -12 8 +-13 -2 -7 -10 6 -12 -2 -11 -33 2 -23 8 -41 13 -41 6 0 5 -4 -3 -9 -13 -8 -13 +-12 0 -27 8 -10 9 -15 2 -11 -8 5 -11 -4 -10 -33 1 -22 5 -40 10 -40 4 0 5 -5 +1 -12 -10 -15 -13 -68 -4 -68 3 0 6 -9 7 -20 1 -12 -3 -18 -9 -14 -6 4 -6 -1 +1 -14 6 -11 11 -37 11 -56 0 -20 3 -36 8 -36 4 0 8 -10 8 -22 1 -25 11 -91 15 +-95 1 -2 2 -6 3 -10 7 -37 51 -153 58 -153 4 0 8 -11 8 -25 0 -14 5 -25 10 +-25 6 0 10 -9 10 -20 0 -11 5 -20 10 -20 6 0 10 -7 10 -17 0 -9 11 -31 26 -50 +14 -18 21 -33 17 -33 -4 0 -3 -4 2 -8 6 -4 23 -26 39 -49 16 -24 50 -65 76 +-93 26 -27 45 -50 43 -50 -2 0 11 -15 29 -34 18 -18 36 -31 40 -29 5 2 8 -3 8 +-11 0 -9 5 -16 10 -16 6 0 22 -11 35 -25 14 -13 25 -22 25 -20 0 3 9 -3 19 +-12 38 -35 50 -43 65 -43 7 0 17 -7 20 -16 4 -9 9 -14 12 -11 4 3 13 -1 21 -9 +9 -8 22 -13 29 -10 8 3 11 0 7 -10 -3 -9 1 -14 13 -14 9 0 25 -7 34 -15 9 -8 +29 -16 43 -17 15 -1 27 -5 27 -8 0 -3 15 -11 33 -18 17 -8 41 -18 52 -23 11 +-6 30 -11 43 -12 12 0 22 -6 22 -11 0 -5 3 -7 6 -4 3 3 31 -1 62 -10 31 -8 68 +-16 82 -16 14 -1 32 -6 40 -12 22 -13 4543 -18 4635 -4 109 16 124 19 195 39 +104 29 180 59 192 76 4 5 8 7 8 2 0 -4 11 0 25 9 14 9 25 13 25 9 0 -4 7 -1 +16 6 17 14 49 34 74 45 19 9 147 102 178 129 58 50 112 108 112 117 0 6 3 9 6 +5 7 -7 60 54 56 65 -1 4 4 8 11 8 7 0 31 27 52 61 22 33 48 72 57 87 10 15 23 +39 29 55 6 15 15 27 20 27 4 0 6 7 3 15 -4 8 -1 15 5 15 6 0 11 6 11 13 0 6 6 +24 14 38 13 25 45 116 47 131 0 4 5 22 10 40 13 42 19 67 23 98 28 185 29 234 +22 2430 -11 3831 -10 3729 -33 3780 -3 8 -7 22 -8 30 -1 8 -5 22 -8 30 -4 8 +-8 26 -11 40 -2 14 -10 39 -16 55 -33 79 -44 104 -68 155 -36 76 -110 190 +-123 190 -6 0 -9 5 -7 11 5 14 -54 78 -65 72 -4 -3 -7 -1 -6 4 3 16 -1 36 -6 +31 -3 -3 -36 24 -73 58 -37 35 -71 64 -75 64 -5 0 -21 12 -36 28 -15 15 -41 +34 -57 42 -16 8 -38 23 -48 33 -11 9 -25 17 -32 17 -6 0 -17 7 -24 15 -7 8 +-16 12 -21 9 -5 -3 -9 0 -9 5 0 6 -9 11 -20 11 -11 0 -20 5 -20 10 0 6 -9 10 +-20 10 -11 0 -20 4 -20 8 0 9 -119 52 -144 52 -9 0 -16 4 -16 8 0 7 -24 12 +-67 15 -7 0 -13 3 -13 7 0 4 -6 7 -12 7 -7 1 -39 5 -70 10 -40 7 -61 7 -69 -1 +-9 -8 -10 -8 -5 2 5 8 0 12 -13 12 -12 0 -21 -6 -21 -12 0 -10 -2 -10 -9 0 -5 +9 -31 13 -77 13 -38 0 -181 2 -319 5 -236 5 -252 7 -285 28 -59 38 -205 100 +-230 99 -3 0 -16 3 -29 8 -69 27 -260 34 -788 29 -100 -1 -187 1 -193 5 -11 7 +11 35 27 35 15 0 143 141 137 151 -3 5 -1 9 4 9 11 0 52 81 52 103 0 9 5 19 +11 22 6 4 8 13 5 20 -2 7 -2 16 2 19 16 16 24 183 10 202 -5 7 -8 16 -7 20 1 +5 0 12 -1 17 -3 9 -4 13 -15 52 -15 58 -107 200 -138 213 -4 2 -22 17 -40 33 +-17 16 -37 29 -44 29 -7 0 -13 4 -13 8 0 5 -10 13 -22 19 -13 6 -36 17 -53 24 +-16 8 -36 20 -43 27 -20 18 -330 17 -357 -2z m245 -329 c34 -7 60 -19 60 -27 +0 -5 9 -12 21 -15 11 -4 18 -9 15 -12 -3 -3 0 -11 7 -19 32 -35 37 -45 39 -69 +1 -14 5 -25 8 -25 3 0 5 -13 5 -30 1 -16 -2 -30 -6 -30 -4 0 -6 -11 -4 -25 1 +-14 -1 -25 -6 -25 -4 0 -10 -8 -14 -19 -6 -21 -55 -77 -55 -64 0 4 -4 3 -8 -3 +-12 -19 -70 -44 -102 -44 -22 0 -29 -4 -24 -12 6 -10 5 -10 -8 -1 -8 7 -27 14 +-42 15 -14 1 -26 6 -26 10 0 4 -6 8 -13 8 -7 0 -23 9 -34 19 -12 11 -24 17 +-27 14 -3 -4 -6 1 -6 10 0 10 -4 17 -9 17 -26 0 -60 164 -38 181 4 3 10 16 12 +30 3 13 9 25 15 27 6 2 9 7 7 10 -2 4 10 19 25 34 15 15 28 24 28 21 0 -3 7 0 +16 8 25 21 103 28 164 16z"/> +</g> +</svg> diff --git a/src/manifest/site.webmanifest b/src/manifest/site.webmanifest new file mode 100644 index 0000000..ada3b78 --- /dev/null +++ b/src/manifest/site.webmanifest @@ -0,0 +1,24 @@ +{ + "name": "Donetick: Simplify Tasks & Chores, Together.", + "short_name": "Donetick", + "description": "An open-source, user-friendly app for managing tasks and chores, featuring customizable options to help you and others stay organized", + "start_url": "/index.html", + "scope": "/", + "lang": "en", + + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/src/service/AuthenticationService.jsx b/src/service/AuthenticationService.jsx new file mode 100644 index 0000000..924de11 --- /dev/null +++ b/src/service/AuthenticationService.jsx @@ -0,0 +1,18 @@ +import React, { createContext, useState } from 'react' + +const AuthenticationContext = createContext({}) + +const AuthenticationProvider = ({ children }) => { + const [isLoggedIn, setIsLoggedIn] = useState(false) + const [userProfile, setUserProfile] = useState({}) + return ( + <AuthenticationContext.Provider + value={{ isLoggedIn, setIsLoggedIn, userProfile, setUserProfile }} + > + {children} + </AuthenticationContext.Provider> + ) +} +export { AuthenticationContext, AuthenticationProvider } + +// export default AuthenticationProvider; diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx new file mode 100644 index 0000000..6444b34 --- /dev/null +++ b/src/utils/Fetcher.jsx @@ -0,0 +1,250 @@ +import { API_URL } from '../Config' +import { Fetch, HEADERS } from './TokenManager' + +const createChore = userID => { + return Fetch(`${API_URL}/chores/`, { + method: 'POST', + headers: HEADERS(), + body: JSON.stringify({ + createdBy: Number(userID), + }), + }).then(response => response.json()) +} + +const signUp = (username, password, displayName, email) => { + return fetch(`${API_URL}/auth/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password, displayName, email }), + }) +} + +const login = (username, password) => { + return fetch(`${API_URL}/auth/login`, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ username, password }), + }) +} + +const GetAllUsers = () => { + return fetch(`${API_URL}/users/`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const GetChores = () => { + return Fetch(`${API_URL}/chores/`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const GetChoreByID = id => { + return Fetch(`${API_URL}/chores/${id}`, { + method: 'GET', + headers: HEADERS(), + }) +} +const CreateChore = chore => { + return Fetch(`${API_URL}/chores/`, { + method: 'POST', + headers: HEADERS(), + body: JSON.stringify(chore), + }) +} + +const DeleteChore = id => { + return Fetch(`${API_URL}/chores/${id}`, { + method: 'DELETE', + headers: HEADERS(), + }) +} + +const SaveChore = chore => { + console.log('chore', chore) + return Fetch(`${API_URL}/chores/`, { + method: 'PUT', + headers: HEADERS(), + body: JSON.stringify(chore), + }) +} +const GetChoreHistory = choreId => { + return Fetch(`${API_URL}/chores/${choreId}/history`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const GetAllCircleMembers = () => { + return Fetch(`${API_URL}/circles/members`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const GetUserProfile = () => { + return Fetch(`${API_URL}/users/profile`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const GetUserCircle = () => { + return Fetch(`${API_URL}/circles/`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const JoinCircle = inviteCode => { + return Fetch(`${API_URL}/circles/join?invite_code=${inviteCode}`, { + method: 'POST', + headers: HEADERS(), + }) +} + +const GetCircleMemberRequests = () => { + return Fetch(`${API_URL}/circles/members/requests`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const AcceptCircleMemberRequest = id => { + return Fetch(`${API_URL}/circles/members/requests/accept?requestId=${id}`, { + method: 'PUT', + headers: HEADERS(), + }) +} + +const LeaveCircle = id => { + return Fetch(`${API_URL}/circles/leave?circle_id=${id}`, { + method: 'DELETE', + headers: HEADERS(), + }) +} + +const DeleteCircleMember = (circleID, memberID) => { + return Fetch( + `${API_URL}/circles/${circleID}/members/delete?member_id=${memberID}`, + { + method: 'DELETE', + headers: HEADERS(), + }, + ) +} + +const UpdateUserDetails = userDetails => { + return Fetch(`${API_URL}/users`, { + method: 'PUT', + headers: HEADERS(), + body: JSON.stringify(userDetails), + }) +} + +const GetSubscriptionSession = () => { + return Fetch(API_URL + `/payments/create-subscription`, { + method: 'GET', + headers: HEADERS(), + }) +} + +const CancelSubscription = () => { + return Fetch(API_URL + `/payments/cancel-subscription`, { + method: 'POST', + headers: HEADERS(), + }) +} + +const GetThings = () => { + return Fetch(`${API_URL}/things`, { + method: 'GET', + headers: HEADERS(), + }) +} +const CreateThing = thing => { + return Fetch(`${API_URL}/things`, { + method: 'POST', + headers: HEADERS(), + body: JSON.stringify(thing), + }) +} + +const SaveThing = thing => { + return Fetch(`${API_URL}/things`, { + method: 'PUT', + headers: HEADERS(), + body: JSON.stringify(thing), + }) +} + +const UpdateThingState = thing => { + return Fetch(`${API_URL}/things/${thing.id}/state?value=${thing.state}`, { + method: 'PUT', + headers: HEADERS(), + }) +} +const DeleteThing = id => { + return Fetch(`${API_URL}/things/${id}`, { + method: 'DELETE', + headers: HEADERS(), + }) +} + +const CreateLongLiveToken = name => { + return Fetch(`${API_URL}/users/tokens`, { + method: 'POST', + headers: HEADERS(), + body: JSON.stringify({ name }), + }) +} +const DeleteLongLiveToken = id => { + return Fetch(`${API_URL}/users/tokens/${id}`, { + method: 'DELETE', + headers: HEADERS(), + }) +} + +const GetLongLiveTokens = () => { + return Fetch(`${API_URL}/users/tokens`, { + method: 'GET', + headers: HEADERS(), + }) +} +export { + AcceptCircleMemberRequest, + CancelSubscription, + createChore, + CreateChore, + CreateLongLiveToken, + CreateThing, + DeleteChore, + DeleteCircleMember, + DeleteLongLiveToken, + DeleteThing, + GetAllCircleMembers, + GetAllUsers, + GetChoreByID, + GetChoreHistory, + GetChores, + GetCircleMemberRequests, + GetLongLiveTokens, + GetSubscriptionSession, + GetThings, + GetUserCircle, + GetUserProfile, + JoinCircle, + LeaveCircle, + login, + SaveChore, + SaveThing, + signUp, + UpdateThingState, + UpdateUserDetails, +} diff --git a/src/utils/Helpers.jsx b/src/utils/Helpers.jsx new file mode 100644 index 0000000..17c0351 --- /dev/null +++ b/src/utils/Helpers.jsx @@ -0,0 +1,7 @@ +import moment from 'moment' + +const isPlusAccount = userProfile => { + return userProfile?.expiration && moment(userProfile?.expiration).isAfter() +} + +export { isPlusAccount } diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx new file mode 100644 index 0000000..35472e4 --- /dev/null +++ b/src/utils/TokenManager.jsx @@ -0,0 +1,65 @@ +import Cookies from 'js-cookie' +import { API_URL } from '../Config' +export function Fetch(url, options) { + if (!isTokenValid()) { + console.log('FETCH: Token is not valid') + console.log(localStorage.getItem('ca_token')) + // store current location in cookie + Cookies.set('ca_redirect', window.location.pathname) + // Assuming you have a function isTokenValid() that checks token validity + window.location.href = '/login' // Redirect to login page + // return Promise.reject("Token is not valid"); + } + if (!options) { + options = {} + } + options.headers = { ...options.headers, ...HEADERS() } + + return fetch(url, options) +} + +export const HEADERS = () => { + return { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + localStorage.getItem('ca_token'), + } +} + +export const isTokenValid = () => { + const expiration = localStorage.getItem('ca_expiration') + const token = localStorage.getItem('ca_token') + + if (localStorage.getItem('ca_token')) { + const now = new Date() + const expire = new Date(expiration) + if (now < expire) { + if (now.getTime() + 24 * 60 * 60 * 1000 > expire.getTime()) { + refreshAccessToken() + } + + return true + } else { + localStorage.removeItem('ca_token') + localStorage.removeItem('ca_expiration') + } + return false + } +} + +export const refreshAccessToken = () => { + fetch(API_URL + '/auth/refresh', { + method: 'GET', + headers: HEADERS(), + }).then(res => { + if (res.status === 200) { + res.json().then(data => { + localStorage.setItem('ca_token', data.token) + localStorage.setItem('ca_expiration', data.expire) + }) + } else { + return res.json().then(error => { + console.log(error) + }) + } + }) +} diff --git a/src/views/Authorization/AuthorizationContainer.jsx b/src/views/Authorization/AuthorizationContainer.jsx new file mode 100644 index 0000000..3bfc622 --- /dev/null +++ b/src/views/Authorization/AuthorizationContainer.jsx @@ -0,0 +1,45 @@ +// import Logo from 'Components/Logo' +import { Box, Paper } from '@mui/material' +import { styled } from '@mui/material/styles' + +const Container = styled('div')(({ theme }) => ({ + minHeight: '100vh', + padding: '24px', + display: 'grid', + placeItems: 'start center', + [theme.breakpoints.up('sm')]: { + // center children + placeItems: 'center', + }, +})) + +const AuthCard = styled(Paper)(({ theme }) => ({ + // border: "1px solid #c4c4c4", + padding: 24, + paddingTop: 32, + borderRadius: 24, + width: '100%', + maxWidth: '400px', + [theme.breakpoints.down('sm')]: { + maxWidth: 'unset', + }, +})) + +export default function AuthCardContainer({ children, ...props }) { + return ( + <Container> + <AuthCard elevation={0}> + <Box + sx={{ + display: 'grid', + placeItems: 'center', + paddingBottom: 4, + }} + > + {/* <Logo size='96px' /> */} + </Box> + {children} + </AuthCard> + </Container> + ) +} diff --git a/src/views/Authorization/ForgotPasswordView.jsx b/src/views/Authorization/ForgotPasswordView.jsx new file mode 100644 index 0000000..44601eb --- /dev/null +++ b/src/views/Authorization/ForgotPasswordView.jsx @@ -0,0 +1,227 @@ +// create boilerplate for ResetPasswordView: +import { + Box, + Button, + Container, + FormControl, + FormHelperText, + Input, + Sheet, + Snackbar, + Typography, +} from '@mui/joy' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import { API_URL } from './../../Config' + +const ForgotPasswordView = () => { + const navigate = useNavigate() + // const [showLoginSnackbar, setShowLoginSnackbar] = useState(false) + // const [snackbarMessage, setSnackbarMessage] = useState('') + const [resetStatusOk, setResetStatusOk] = useState(null) + const [email, setEmail] = useState('') + const [emailError, setEmailError] = useState(null) + + const validateEmail = email => { + return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email) + } + + const handleSubmit = async () => { + if (!email) { + return setEmailError('Email is required') + } + + // validate email: + if (validateEmail(email)) { + setEmailError('Please enter a valid email address') + return + } + + if (emailError) { + return + } + + try { + const response = await fetch(`${API_URL}/auth/reset`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: email }), + }) + + if (response.ok) { + setResetStatusOk(true) + // wait 3 seconds and then redirect to login: + } else { + setResetStatusOk(false) + } + } catch (error) { + setResetStatusOk(false) + } + } + + const handleEmailChange = e => { + setEmail(e.target.value) + if (validateEmail(e.target.value)) { + setEmailError('Please enter a valid email address') + } else { + setEmailError(null) + } + } + + return ( + <Container + component='main' + maxWidth='xs' + + // make content center in the middle of the page: + > + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + + justifyContent: 'space-between', + alignItems: 'center', + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + minHeight: '70vh', + justifyContent: 'space-between', + justifyItems: 'center', + }} + > + <Box> + <img + src='/src/assets/logo.svg' + alt='logo' + width='128px' + height='128px' + /> + {/* <Logo /> */} + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + </Box> + {/* HERE */} + <Box sx={{ textAlign: 'center' }}></Box> + {resetStatusOk === null && ( + <form onSubmit={handleSubmit}> + <div className='grid gap-6'> + <Typography level='body2' gutterBottom> + Enter your email, and we'll send you a link to get into your + account. + </Typography> + <FormControl error={emailError !== null}> + <Input + placeholder='Email' + type='email' + variant='soft' + fullWidth + size='lg' + value={email} + onChange={handleEmailChange} + error={emailError !== null} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault() + handleSubmit() + } + }} + /> + <FormHelperText>{emailError}</FormHelperText> + </FormControl> + <Box> + <Button + variant='solid' + size='lg' + fullWidth + sx={{ + mb: 1, + }} + onClick={handleSubmit} + > + Reset Password + </Button> + <Button + fullWidth + size='lg' + variant='soft' + sx={{ + width: '100%', + border: 'moccasin', + borderRadius: '8px', + }} + onClick={() => { + navigate('/login') + }} + color='neutral' + > + Back to Login + </Button> + </Box> + </div> + </form> + )} + {resetStatusOk != null && ( + <> + <Box mt={-30}> + <Typography level='body-md'> + if there is an account associated with the email you entered, + you will receive an email with instructions on how to reset + your + </Typography> + </Box> + <Button + variant='soft' + size='lg' + sx={{ position: 'relative', bottom: '0' }} + onClick={() => { + navigate('/login') + }} + fullWidth + > + Go to Login + </Button> + </> + )} + <Snackbar + open={resetStatusOk ? resetStatusOk : resetStatusOk === false} + autoHideDuration={5000} + onClose={() => { + if (resetStatusOk) { + navigate('/login') + } + }} + > + {resetStatusOk + ? 'Reset email sent, check your email' + : 'Reset email failed, try again later'} + </Snackbar> + </Sheet> + </Box> + </Container> + ) +} + +export default ForgotPasswordView diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx new file mode 100644 index 0000000..2ffcef4 --- /dev/null +++ b/src/views/Authorization/LoginView.jsx @@ -0,0 +1,345 @@ +import GoogleIcon from '@mui/icons-material/Google' +import { + Avatar, + Box, + Button, + Container, + Divider, + Input, + Sheet, + Snackbar, + Typography, +} from '@mui/joy' +import Cookies from 'js-cookie' +import React from 'react' +import { useNavigate } from 'react-router-dom' +import { LoginSocialGoogle } from 'reactjs-social-login' +import { API_URL, GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config' +import { UserContext } from '../../contexts/UserContext' +import Logo from '../../Logo' +import { GetUserProfile } from '../../utils/Fetcher' +const LoginView = () => { + const { userProfile, setUserProfile } = React.useContext(UserContext) + const [username, setUsername] = React.useState('') + const [password, setPassword] = React.useState('') + const [error, setError] = React.useState(null) + const Navigate = useNavigate() + const handleSubmit = async e => { + e.preventDefault() + + fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }) + .then(response => { + if (response.status === 200) { + return response.json().then(data => { + localStorage.setItem('ca_token', data.token) + localStorage.setItem('ca_expiration', data.expire) + const redirectUrl = Cookies.get('ca_redirect') + // console.log('redirectUrl', redirectUrl) + if (redirectUrl) { + Cookies.remove('ca_redirect') + Navigate(redirectUrl) + } else { + Navigate('/my/chores') + } + }) + } else if (response.status === 401) { + setError('Wrong username or password') + } else { + setError('An error occurred, please try again') + console.log('Login failed') + } + }) + .catch(err => { + setError('Unable to communicate with server, please try again') + console.log('Login failed', err) + }) + } + + const loggedWithProvider = function (provider, data) { + console.log(provider, data) + return fetch(API_URL + `/auth/${provider}/callback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + provider: provider, + token: + data['access_token'] || // data["access_token"] is for Google + data['accessToken'], // data["accessToken"] is for Facebook + data: data, + }), + }).then(response => { + if (response.status === 200) { + return response.json().then(data => { + localStorage.setItem('ca_token', data.token) + localStorage.setItem('ca_expiration', data.expire) + // setIsLoggedIn(true); + getUserProfileAndNavigateToHome() + }) + } + return response.json().then(error => { + setError("Couldn't log in with Google, please try again") + }) + }) + } + const getUserProfileAndNavigateToHome = () => { + GetUserProfile().then(data => { + data.json().then(data => { + setUserProfile(data.res) + // check if redirect url is set in cookie: + const redirectUrl = Cookies.get('ca_redirect') + if (redirectUrl) { + Cookies.remove('ca_redirect') + Navigate(redirectUrl) + } else { + Navigate('/my/chores') + } + }) + }) + } + const handleForgotPassword = () => { + Navigate('/forgot-password') + } + return ( + <Container + component='main' + maxWidth='xs' + + // make content center in the middle of the page: + > + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + {/* <img + src='/src/assets/logo.svg' + alt='logo' + width='128px' + height='128px' + /> */} + <Logo /> + + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + + {userProfile && ( + <> + <Avatar + src={userProfile?.image} + alt={userProfile?.username} + size='lg' + sx={{ + mt: 2, + width: '96px', + height: '96px', + mb: 1, + }} + /> + <Typography level='body-md' alignSelf={'center'}> + Welcome back,{' '} + {userProfile?.displayName || userProfile?.username} + </Typography> + + <Button + fullWidth + size='lg' + sx={{ mt: 3, mb: 2 }} + onClick={() => { + getUserProfileAndNavigateToHome() + }} + > + Continue as {userProfile.displayName || userProfile.username} + </Button> + <Button + type='submit' + fullWidth + size='lg' + q + variant='plain' + sx={{ + width: '100%', + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={() => { + setUserProfile(null) + localStorage.removeItem('ca_token') + localStorage.removeItem('ca_expiration') + // go to login page: + window.location.href = '/login' + }} + > + Logout + </Button> + </> + )} + {!userProfile && ( + <> + <Typography level='body2'> + Sign in to your account to continue + </Typography> + <Typography level='body2' alignSelf={'start'} mt={4}> + Username + </Typography> + <Input + margin='normal' + required + fullWidth + id='email' + label='Email Address' + name='email' + autoComplete='email' + autoFocus + value={username} + onChange={e => { + setUsername(e.target.value) + }} + /> + <Typography level='body2' alignSelf={'start'}> + Password: + </Typography> + <Input + margin='normal' + required + fullWidth + name='password' + label='Password' + type='password' + id='password' + value={password} + onChange={e => { + setPassword(e.target.value) + }} + /> + + <Button + type='submit' + fullWidth + size='lg' + variant='solid' + sx={{ + width: '100%', + mt: 3, + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={handleSubmit} + > + Sign In + </Button> + <Button + type='submit' + fullWidth + size='lg' + q + variant='plain' + sx={{ + width: '100%', + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={handleForgotPassword} + > + Forgot password? + </Button> + </> + )} + <Divider> or </Divider> + + <Box sx={{ width: '100%' }}> + <LoginSocialGoogle + client_id={GOOGLE_CLIENT_ID} + redirect_uri={REDIRECT_URL} + scope='openid profile email' + discoveryDocs='claims_supported' + access_type='online' + isOnlyGetToken={true} + onResolve={({ provider, data }) => { + loggedWithProvider(provider, data) + }} + onReject={err => { + setError("Couldn't log in with Google, please try again") + }} + > + <Button + variant='soft' + color='neutral' + size='lg' + fullWidth + sx={{ + width: '100%', + mt: 1, + mb: 1, + border: 'moccasin', + borderRadius: '8px', + }} + > + <div className='flex gap-2'> + <GoogleIcon /> + Continue with Google + </div> + </Button> + </LoginSocialGoogle> + </Box> + + <Button + onClick={() => { + Navigate('/signup') + }} + fullWidth + variant='soft' + size='lg' + // sx={{ mt: 3, mb: 2 }} + > + Create new account + </Button> + </Sheet> + </Box> + <Snackbar + open={error !== null} + onClose={() => setError(null)} + autoHideDuration={3000} + message={error} + > + {error} + </Snackbar> + </Container> + ) +} + +export default LoginView diff --git a/src/views/Authorization/Signup.jsx b/src/views/Authorization/Signup.jsx new file mode 100644 index 0000000..d83411f --- /dev/null +++ b/src/views/Authorization/Signup.jsx @@ -0,0 +1,243 @@ +import { + Box, + Button, + Container, + Divider, + FormControl, + FormHelperText, + Input, + Sheet, + Typography, +} from '@mui/joy' +import React from 'react' +import { useNavigate } from 'react-router-dom' +import Logo from '../../Logo' +import { login, signUp } from '../../utils/Fetcher' + +const SignupView = () => { + const [username, setUsername] = React.useState('') + const [password, setPassword] = React.useState('') + const Navigate = useNavigate() + const [displayName, setDisplayName] = React.useState('') + const [email, setEmail] = React.useState('') + const [usernameError, setUsernameError] = React.useState('') + const [passwordError, setPasswordError] = React.useState('') + const [emailError, setEmailError] = React.useState('') + const [displayNameError, setDisplayNameError] = React.useState('') + const [error, setError] = React.useState(null) + const handleLogin = (username, password) => { + login(username, password).then(response => { + if (response.status === 200) { + response.json().then(res => { + localStorage.setItem('ca_token', res.token) + localStorage.setItem('ca_expiration', res.expire) + setTimeout(() => { + // TODO: not sure if there is a race condition here + // but on first sign up it renavigates to login. + Navigate('/my/chores') + }, 500) + }) + } else { + console.log('Login failed', response) + // Navigate('/login') + } + }) + } + const handleSignUpValidation = () => { + // Reset errors before validation + setUsernameError(null) + setPasswordError(null) + setDisplayNameError(null) + setEmailError(null) + + let isValid = true + + if (!username.trim()) { + setUsernameError('Username is required') + isValid = false + } + if (username.length < 4) { + setUsernameError('Username must be at least 4 characters') + isValid = false + } + // if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + // setEmailError('Invalid email address') + // isValid = false + // } + + if (password.length < 8) { + setPasswordError('Password must be at least 8 characters') + isValid = false + } + + if (!displayName.trim()) { + setDisplayNameError('Display name is required') + isValid = false + } + + // display name should only contain letters and spaces and numbers: + if (!/^[a-zA-Z0-9 ]+$/.test(displayName)) { + setDisplayNameError('Display name can only contain letters and numbers') + isValid = false + } + + // username should only contain letters , numbers , dot and dash: + if (!/^[a-zA-Z0-9.-]+$/.test(username)) { + setUsernameError( + 'Username can only contain letters, numbers, dot and dash', + ) + isValid = false + } + + return isValid + } + const handleSubmit = async e => { + e.preventDefault() + if (!handleSignUpValidation()) { + return + } + signUp(username, password, displayName, email).then(response => { + if (response.status === 201) { + handleLogin(username, password) + } else { + console.log('Signup failed') + setError('Signup failed') + } + }) + } + + return ( + <Container component='main' maxWidth='xs'> + <Box + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginTop: 4, + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + // alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + <Box + sx={{ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + }} + > + <Logo /> + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + <Typography level='body2'> + Create an account to get started! + </Typography> + </Box> + <Typography level='body2' alignSelf={'start'} mt={4}> + Username + </Typography> + <Input + margin='normal' + required + fullWidth + id='email' + label='Email Address' + name='email' + autoComplete='email' + autoFocus + value={username} + onChange={e => { + setUsernameError(null) + setUsername(e.target.value.trim()) + }} + /> + <FormControl error={usernameError}> + <FormHelperText c>{usernameError}</FormHelperText> + </FormControl> + {/* Error message display */} + <Typography level='body2' alignSelf={'start'}> + Password: + </Typography> + <Input + margin='normal' + required + fullWidth + name='password' + label='Password' + type='password' + id='password' + value={password} + onChange={e => { + setPasswordError(null) + setPassword(e.target.value) + }} + /> + <FormControl error={passwordError}> + <FormHelperText>{passwordError}</FormHelperText> + </FormControl> + <Typography level='body2' alignSelf={'start'}> + Display Name: + </Typography> + <Input + margin='normal' + required + fullWidth + name='displayName' + label='Display Name' + id='displayName' + value={displayName} + onChange={e => { + setDisplayNameError(null) + setDisplayName(e.target.value) + }} + /> + <FormControl error={displayNameError}> + <FormHelperText>{displayNameError}</FormHelperText> + </FormControl> + <Button + // type='submit' + size='lg' + fullWidth + variant='solid' + sx={{ mt: 3, mb: 1 }} + onClick={handleSubmit} + > + Sign Up + </Button> + <Divider> or </Divider> + <Button + size='lg' + onClick={() => { + Navigate('/login') + }} + fullWidth + variant='soft' + // sx={{ mt: 3, mb: 2 }} + > + Login + </Button> + </Sheet> + </Box> + </Container> + ) +} + +export default SignupView diff --git a/src/views/Authorization/UpdatePasswordView.jsx b/src/views/Authorization/UpdatePasswordView.jsx new file mode 100644 index 0000000..7177f2f --- /dev/null +++ b/src/views/Authorization/UpdatePasswordView.jsx @@ -0,0 +1,194 @@ +// create boilerplate for ResetPasswordView: +import { + Box, + Button, + Container, + FormControl, + FormHelperText, + Input, + Sheet, + Snackbar, + Typography, +} from '@mui/joy' +import { useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' + +import { API_URL } from '../../Config' +import Logo from '../../Logo' + +const UpdatePasswordView = () => { + const navigate = useNavigate() + const [password, setPassword] = useState('') + const [passwordConfirm, setPasswordConfirm] = useState('') + const [passwordError, setPasswordError] = useState(null) + const [passworConfirmationError, setPasswordConfirmationError] = + useState(null) + const [searchParams] = useSearchParams() + + const [updateStatusOk, setUpdateStatusOk] = useState(null) + + const verifiticationCode = searchParams.get('c') + + const handlePasswordChange = e => { + const password = e.target.value + setPassword(password) + if (password.length < 8) { + setPasswordError('Password must be at least 8 characters') + } else { + setPasswordError(null) + } + } + const handlePasswordConfirmChange = e => { + setPasswordConfirm(e.target.value) + if (e.target.value !== password) { + setPasswordConfirmationError('Passwords do not match') + } else { + setPasswordConfirmationError(null) + } + } + + const handleSubmit = async () => { + if (passwordError != null || passworConfirmationError != null) { + return + } + try { + const response = await fetch( + `${API_URL}/auth/password?c=${verifiticationCode}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ password: password }), + }, + ) + + if (response.ok) { + setUpdateStatusOk(true) + // wait 3 seconds and then redirect to login: + setTimeout(() => { + navigate('/login') + }, 3000) + } else { + setUpdateStatusOk(false) + } + } catch (error) { + setUpdateStatusOk(false) + } + } + return ( + <Container component='main' maxWidth='xs'> + <Box + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginTop: 4, + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + // alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + <Box + sx={{ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + }} + > + <Logo /> + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + <Typography level='body2' mb={4}> + Please enter your new password below + </Typography> + </Box> + + <FormControl error> + <Input + placeholder='Password' + type='password' + value={password} + onChange={handlePasswordChange} + error={passwordError !== null} + // onKeyDown={e => { + // if (e.key === 'Enter' && validateForm(validateFormInput)) { + // handleSubmit(e) + // } + // }} + /> + <FormHelperText>{passwordError}</FormHelperText> + </FormControl> + + <FormControl error> + <Input + placeholder='Confirm Password' + type='password' + value={passwordConfirm} + onChange={handlePasswordConfirmChange} + error={passworConfirmationError !== null} + // onKeyDown={e => { + // if (e.key === 'Enter' && validateForm(validateFormInput)) { + // handleSubmit(e) + // } + // }} + /> + <FormHelperText>{passworConfirmationError}</FormHelperText> + </FormControl> + {/* helper to show password not matching : */} + + <Button + fullWidth + size='lg' + sx={{ + mt: 5, + mb: 1, + }} + onClick={handleSubmit} + > + Save Password + </Button> + <Button + fullWidth + size='lg' + variant='soft' + onClick={() => { + navigate('/login') + }} + > + Cancel + </Button> + </Sheet> + </Box> + <Snackbar + open={updateStatusOk !== true} + autoHideDuration={6000} + onClose={() => { + setUpdateStatusOk(null) + }} + > + Password update failed, try again later + </Snackbar> + </Container> + ) +} + +export default UpdatePasswordView diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx new file mode 100644 index 0000000..e8eb17d --- /dev/null +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -0,0 +1,744 @@ +import { + Box, + Button, + Card, + Checkbox, + Chip, + Container, + Divider, + FormControl, + FormHelperText, + Input, + List, + ListItem, + Option, + Radio, + RadioGroup, + Select, + Sheet, + Typography, +} from '@mui/joy' +import moment from 'moment' +import { useContext, useEffect, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { UserContext } from '../../contexts/UserContext' +import { + CreateChore, + DeleteChore, + GetAllCircleMembers, + GetChoreByID, + GetChoreHistory, + GetThings, + SaveChore, +} from '../../utils/Fetcher' +import { isPlusAccount } from '../../utils/Helpers' +import FreeSoloCreateOption from '../components/AutocompleteSelect' +import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import RepeatSection from './RepeatSection' +const ASSIGN_STRATEGIES = [ + 'random', + 'least_assigned', + 'least_completed', + 'keep_last_assigned', +] +const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month'] + +const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once'] +const NO_DUE_DATE_ALLOWED_TYPE = ['trigger'] +const ChoreEdit = () => { + const { userProfile, setUserProfile } = useContext(UserContext) + const [chore, setChore] = useState([]) + const [choresHistory, setChoresHistory] = useState([]) + const [userHistory, setUserHistory] = useState({}) + const { choreId } = useParams() + const [name, setName] = useState('') + const [confirmModelConfig, setConfirmModelConfig] = useState({}) + const [assignees, setAssignees] = useState([]) + const [performers, setPerformers] = useState([]) + const [assignStrategy, setAssignStrategy] = useState(ASSIGN_STRATEGIES[2]) + const [dueDate, setDueDate] = useState(null) + const [completed, setCompleted] = useState(false) + const [completedDate, setCompletedDate] = useState('') + const [assignedTo, setAssignedTo] = useState(-1) + const [frequencyType, setFrequencyType] = useState('once') + const [frequency, setFrequency] = useState(1) + const [frequencyMetadata, setFrequencyMetadata] = useState({}) + const [labels, setLabels] = useState([]) + const [allUserThings, setAllUserThings] = useState([]) + const [thingTrigger, setThingTrigger] = useState({}) + const [isThingValid, setIsThingValid] = useState(false) + + const [notificationMetadata, setNotificationMetadata] = useState({}) + + const [isRolling, setIsRolling] = useState(false) + const [isNotificable, setIsNotificable] = useState(false) + const [isActive, setIsActive] = useState(true) + const [updatedBy, setUpdatedBy] = useState(0) + const [createdBy, setCreatedBy] = useState(0) + const [errors, setErrors] = useState({}) + const [attemptToSave, setAttemptToSave] = useState(false) + + const Navigate = useNavigate() + + const HandleValidateChore = () => { + const errors = {} + + if (name.trim() === '') { + errors.name = 'Name is required' + } + if (assignees.length === 0) { + errors.assignees = 'At least 1 assignees is required' + } + if (assignedTo < 0) { + errors.assignedTo = 'Assigned to is required' + } + if (frequencyType === 'interval' && frequency < 1) { + errors.frequency = 'Frequency is required' + } + if ( + frequencyType === 'days_of_the_week' && + frequencyMetadata['days']?.length === 0 + ) { + errors.frequency = 'At least 1 day is required' + } + if ( + frequencyType === 'day_of_the_month' && + frequencyMetadata['months']?.length === 0 + ) { + errors.frequency = 'At least 1 month is required' + } + if ( + dueDate === null && + !NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && + !NO_DUE_DATE_ALLOWED_TYPE.includes(frequencyType) + ) { + if (REPEAT_ON_TYPE.includes(frequencyType)) { + errors.dueDate = 'Start date is required' + } else { + errors.dueDate = 'Due date is required' + } + } + if (frequencyType === 'trigger') { + if (!isThingValid) { + errors.thingTrigger = 'Thing trigger is invalid' + } + } + + // if there is any error then return false: + setErrors(errors) + if (Object.keys(errors).length > 0) { + return false + } + return true + } + + const HandleSaveChore = () => { + setAttemptToSave(true) + if (!HandleValidateChore()) { + console.log('validation failed') + console.log(errors) + return + } + const chore = { + id: Number(choreId), + name: name, + assignees: assignees, + dueDate: dueDate ? new Date(dueDate).toISOString() : null, + frequencyType: frequencyType, + frequency: Number(frequency), + frequencyMetadata: frequencyMetadata, + assignedTo: assignedTo, + assignStrategy: assignStrategy, + isRolling: isRolling, + isActive: isActive, + notification: isNotificable, + labels: labels, + notificationMetadata: notificationMetadata, + thingTrigger: thingTrigger, + } + let SaveFunction = CreateChore + if (choreId > 0) { + SaveFunction = SaveChore + } + + SaveFunction(chore).then(response => { + if (response.status === 200) { + Navigate(`/my/chores`) + } else { + alert('Failed to save chore') + } + }) + } + useEffect(() => { + //fetch performers: + GetAllCircleMembers() + .then(response => response.json()) + .then(data => { + setPerformers(data.res) + }) + GetThings().then(response => { + response.json().then(data => { + setAllUserThings(data.res) + }) + }) + // fetch chores: + if (choreId > 0) { + GetChoreByID(choreId) + .then(response => { + if (response.status !== 200) { + alert('You are not authorized to view this chore.') + Navigate('/my/chores') + return null + } else { + return response.json() + } + }) + .then(data => { + setChore(data.res) + setName(data.res.name ? data.res.name : '') + setAssignees(data.res.assignees ? data.res.assignees : []) + setAssignedTo(data.res.assignedTo) + setFrequencyType( + data.res.frequencyType ? data.res.frequencyType : 'once', + ) + + setFrequencyMetadata(JSON.parse(data.res.frequencyMetadata)) + setFrequency(data.res.frequency) + + setNotificationMetadata(JSON.parse(data.res.notificationMetadata)) + setLabels(data.res.labels ? data.res.labels.split(',') : []) + + setAssignStrategy( + data.res.assignStrategy + ? data.res.assignStrategy + : ASSIGN_STRATEGIES[2], + ) + setIsRolling(data.res.isRolling) + setIsActive(data.res.isActive) + // parse the due date to a string from this format "2021-10-10T00:00:00.000Z" + // use moment.js or date-fns to format the date for to be usable in the input field: + setDueDate( + data.res.nextDueDate + ? moment(data.res.nextDueDate).format('YYYY-MM-DDTHH:mm:ss') + : null, + ) + setUpdatedBy(data.res.updatedBy) + setCreatedBy(data.res.createdBy) + setIsNotificable(data.res.notification) + setThingTrigger(data.res.thingChore) + // setDueDate(data.res.dueDate) + // setCompleted(data.res.completed) + // setCompletedDate(data.res.completedDate) + }) + + // fetch chores history: + GetChoreHistory(choreId) + .then(response => response.json()) + .then(data => { + setChoresHistory(data.res) + const newUserChoreHistory = {} + data.res.forEach(choreHistory => { + if (newUserChoreHistory[choreHistory.completedBy]) { + newUserChoreHistory[choreHistory.completedBy] += 1 + } else { + newUserChoreHistory[choreHistory.completedBy] = 1 + } + }) + + setUserHistory(newUserChoreHistory) + }) + } + // set focus on the first input field: + else { + // new task/ chore set focus on the first input field: + document.querySelector('input').focus() + } + }, []) + + useEffect(() => { + // if frequancy type change to somthing need a due date then set it to the current date: + if (!NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && !dueDate) { + setDueDate(moment(new Date()).format('YYYY-MM-DDTHH:mm:00')) + } + if (NO_DUE_DATE_ALLOWED_TYPE.includes(frequencyType)) { + setDueDate(null) + } + }, [frequencyType]) + + useEffect(() => { + if (assignees.length === 1) { + setAssignedTo(assignees[0].userId) + } + }, [assignees]) + + useEffect(() => { + if (performers.length > 0 && assignees.length === 0) { + setAssignees([ + { + userId: userProfile.id, + }, + ]) + } + }, [performers]) + + // if user resolve the error trigger validation to remove the error message from the respective field + useEffect(() => { + if (attemptToSave) { + HandleValidateChore() + } + }, [assignees, name, frequencyMetadata, attemptToSave, dueDate]) + + const handleDelete = () => { + setConfirmModelConfig({ + isOpen: true, + title: 'Delete Chore', + confirmText: 'Delete', + cancelText: 'Cancel', + message: 'Are you sure you want to delete this chore?', + onClose: isConfirmed => { + if (isConfirmed === true) { + DeleteChore(choreId).then(response => { + if (response.status === 200) { + Navigate('/my/chores') + } else { + alert('Failed to delete chore') + } + }) + } + setConfirmModelConfig({}) + }, + }) + } + return ( + <Container maxWidth='md'> + {/* <Typography level='h3' mb={1.5}> + Edit Chore + </Typography> */} + <Box> + <FormControl error={errors.name}> + <Typography level='h4'>Descritpion :</Typography> + <Typography level='h5'>What is this chore about?</Typography> + <Input value={name} onChange={e => setName(e.target.value)} /> + <FormHelperText error>{errors.name}</FormHelperText> + </FormControl> + </Box> + <Box mt={2}> + <Typography level='h4'>Assignees :</Typography> + <Typography level='h5'>Who can do this chore?</Typography> + <Card> + <List + orientation='horizontal' + wrap + sx={{ + '--List-gap': '8px', + '--ListItem-radius': '20px', + }} + > + {performers?.map((item, index) => ( + <ListItem key={item.id}> + <Checkbox + // disabled={index === 0} + checked={assignees.find(a => a.userId == item.id) != null} + onClick={() => { + if (assignees.find(a => a.userId == item.id)) { + setAssignees(assignees.filter(i => i.userId !== item.id)) + } else { + setAssignees([...assignees, { userId: item.id }]) + } + }} + overlay + disableIcon + variant='soft' + label={item.displayName} + /> + </ListItem> + ))} + </List> + </Card> + <FormControl error={Boolean(errors.assignee)}> + <FormHelperText error>{Boolean(errors.assignee)}</FormHelperText> + </FormControl> + </Box> + {assignees.length > 1 && ( + // this wrap the details that needed if we have more than one assingee + // we need to pick the next assignedTo and also the strategy to pick the next assignee. + // if we have only one then no need to display this section + <> + <Box mt={2}> + <Typography level='h4'>Assigned :</Typography> + <Typography level='h5'> + Who is assigned the next due chore? + </Typography> + + <Select + placeholder={ + assignees.length === 0 + ? 'No Assignees yet can perform this chore' + : 'Select an assignee for this chore' + } + disabled={assignees.length === 0} + value={assignedTo > -1 ? assignedTo : null} + > + {performers + ?.filter(p => assignees.find(a => a.userId == p.userId)) + .map((item, index) => ( + <Option + value={item.id} + key={item.displayName} + onClick={() => { + setAssignedTo(item.id) + }} + > + {item.displayName} + {/* <Chip size='sm' color='neutral' variant='soft'> + </Chip> */} + </Option> + ))} + </Select> + </Box> + <Box mt={2}> + <Typography level='h4'>Picking Mode :</Typography> + <Typography level='h5'> + How to pick the next assignee for the following chore? + </Typography> + + <Card> + <List + orientation='horizontal' + wrap + sx={{ + '--List-gap': '8px', + '--ListItem-radius': '20px', + }} + > + {ASSIGN_STRATEGIES.map((item, idx) => ( + <ListItem key={item}> + <Checkbox + // disabled={index === 0} + checked={assignStrategy === item} + onClick={() => setAssignStrategy(item)} + overlay + disableIcon + variant='soft' + label={item + .split('_') + .map(x => x.charAt(0).toUpperCase() + x.slice(1)) + .join(' ')} + /> + </ListItem> + ))} + </List> + </Card> + </Box> + </> + )} + <RepeatSection + frequency={frequency} + onFrequencyUpdate={setFrequency} + frequencyType={frequencyType} + onFrequencyTypeUpdate={setFrequencyType} + frequencyMetadata={frequencyMetadata} + onFrequencyMetadataUpdate={setFrequencyMetadata} + frequencyError={errors?.frequency} + allUserThings={allUserThings} + onTriggerUpdate={thingUpdate => { + if (thingUpdate === null) { + setThingTrigger(null) + return + } + setThingTrigger({ + triggerState: thingUpdate.triggerState, + condition: thingUpdate.condition, + thingID: thingUpdate.thing.id, + }) + }} + OnTriggerValidate={setIsThingValid} + isAttemptToSave={attemptToSave} + selectedThing={thingTrigger} + /> + + <Box mt={2}> + <Typography level='h4'> + {REPEAT_ON_TYPE.includes(frequencyType) ? 'Start date' : 'Due date'} : + </Typography> + {frequencyType === 'trigger' && !dueDate && ( + <Typography level='body-sm'> + Due Date will be set when the trigger of the thing is met + </Typography> + )} + + {NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && ( + <FormControl sx={{ mt: 1 }}> + <Checkbox + onChange={e => { + if (e.target.checked) { + setDueDate(moment(new Date()).format('YYYY-MM-DDTHH:mm:00')) + } else { + setDueDate(null) + } + }} + defaultChecked={dueDate !== null} + checked={dueDate !== null} + value={dueDate !== null} + overlay + label='Give this task a due date' + /> + <FormHelperText> + task needs to be completed by a specific time. + </FormHelperText> + </FormControl> + )} + {dueDate && ( + <FormControl error={Boolean(errors.dueDate)}> + <Typography level='h5'> + {REPEAT_ON_TYPE.includes(frequencyType) + ? 'When does this chore start?' + : 'When is the next first time this chore is due?'} + </Typography> + <Input + type='datetime-local' + value={dueDate} + onChange={e => { + setDueDate(e.target.value) + }} + /> + <FormHelperText>{errors.dueDate}</FormHelperText> + </FormControl> + )} + </Box> + {!['once', 'no_repeat'].includes(frequencyType) && ( + <Box mt={2}> + <Typography level='h4'>Scheduling Preferences: </Typography> + <Typography level='h5'> + How to reschedule the next due date? + </Typography> + + <RadioGroup name='tiers' sx={{ gap: 1, '& > div': { p: 1 } }}> + <FormControl> + <Radio + overlay + checked={!isRolling} + onClick={() => setIsRolling(false)} + label='Reschedule from due date' + /> + <FormHelperText> + the next task will be scheduled from the original due date, even + if the previous task was completed late + </FormHelperText> + </FormControl> + <FormControl> + <Radio + overlay + checked={isRolling} + onClick={() => setIsRolling(true)} + label='Reschedule from completion date' + /> + <FormHelperText> + the next task will be scheduled from the actual completion date + of the previous task + </FormHelperText> + </FormControl> + </RadioGroup> + </Box> + )} + <Box mt={2}> + <Typography level='h4'>Notifications : </Typography> + <Typography level='h5'> + Get Reminders when this task is due or completed + {!isPlusAccount(userProfile) && ( + <Chip variant='soft' color='warning'> + Not available in Basic Plan + </Chip> + )} + </Typography> + + <FormControl sx={{ mt: 1 }}> + <Checkbox + onChange={e => { + setIsNotificable(e.target.checked) + }} + defaultChecked={isNotificable} + checked={isNotificable} + value={isNotificable} + disabled={!isPlusAccount(userProfile)} + overlay + label='Notify for this task' + /> + <FormHelperText + sx={{ + opacity: !isPlusAccount(userProfile) ? 0.5 : 1, + }} + > + Receive notifications for this task + </FormHelperText> + </FormControl> + </Box> + {isNotificable && ( + <Box + ml={4} + sx={{ + display: 'flex', + flexDirection: 'column', + gap: 2, + + '& > div': { p: 2, borderRadius: 'md', display: 'flex' }, + }} + > + <Card variant='outlined'> + <Typography level='h5'> + What things should trigger the notification? + </Typography> + {[ + { + title: 'Due Date/Time', + description: 'A simple reminder that a task is due', + id: 'dueDate', + }, + // { + // title: 'Upon Completion', + // description: 'A notification when a task is completed', + // id: 'completion', + // }, + { + title: 'Predued', + description: 'before a task is due in few hours', + id: 'predue', + }, + { + title: 'Overdue', + description: 'A notification when a task is overdue', + }, + { + title: 'Nagging', + description: 'Daily reminders until the task is completed', + id: 'nagging', + }, + ].map(item => ( + <FormControl sx={{ mb: 1 }} key={item.id}> + <Checkbox + overlay + onClick={() => { + setNotificationMetadata({ + ...notificationMetadata, + [item.id]: !notificationMetadata[item.id], + }) + }} + checked={ + notificationMetadata ? notificationMetadata[item.id] : false + } + label={item.title} + key={item.title} + /> + <FormHelperText>{item.description}</FormHelperText> + </FormControl> + ))} + </Card> + </Box> + )} + <Box mt={2}> + <Typography level='h4'>Labels :</Typography> + <Typography level='h5'> + Things to remember about this chore or to tag it + </Typography> + <FreeSoloCreateOption + options={labels} + onSelectChange={changes => { + const newLabels = [] + changes.map(change => { + // if type is string : + if (typeof change === 'string') { + // add the change to the labels array: + newLabels.push(change) + } else { + newLabels.push(change.inputValue) + } + }) + setLabels(newLabels) + }} + /> + </Box> + {choreId > 0 && ( + <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> + <Sheet + sx={{ + p: 2, + borderRadius: 'md', + boxShadow: 'sm', + }} + > + <Typography level='body1'> + Created by{' '} + <Chip variant='solid'> + {performers.find(f => f.id === createdBy)?.displayName} + </Chip>{' '} + {moment(chore.createdAt).fromNow()} + </Typography> + {(chore.updatedAt && updatedBy > 0 && ( + <> + <Divider sx={{ my: 1 }} /> + + <Typography level='body1'> + Updated by{' '} + <Chip variant='solid'> + {performers.find(f => f.id === updatedBy)?.displayName} + </Chip>{' '} + {moment(chore.updatedAt).fromNow()} + </Typography> + </> + )) || <></>} + </Sheet> + </Box> + )} + <Divider sx={{ mb: 9 }} /> + + {/* <Box mt={2} alignSelf={'flex-start'} display='flex' gap={2}> + <Button onClick={SaveChore}>Save</Button> + </Box> */} + <Sheet + variant='outlined' + sx={{ + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + p: 2, // padding + display: 'flex', + justifyContent: 'flex-end', + gap: 2, + 'z-index': 1000, + bgcolor: 'background.body', + boxShadow: 'md', // Add a subtle shadow + }} + > + {choreId > 0 && ( + <Button + color='danger' + variant='solid' + onClick={() => { + // confirm before deleting: + handleDelete() + }} + > + Delete + </Button> + )} + <Button + color='neutral' + variant='outlined' + onClick={() => { + window.history.back() + }} + > + Cancel + </Button> + <Button color='primary' variant='solid' onClick={HandleSaveChore}> + {choreId > 0 ? 'Save' : 'Create'} + </Button> + </Sheet> + <ConfirmationModal config={confirmModelConfig} /> + {/* <ChoreHistory ChoreHistory={choresHistory} UsersData={performers} /> */} + </Container> + ) +} + +export default ChoreEdit diff --git a/src/views/ChoreEdit/RepeatSection.jsx b/src/views/ChoreEdit/RepeatSection.jsx new file mode 100644 index 0000000..cb680eb --- /dev/null +++ b/src/views/ChoreEdit/RepeatSection.jsx @@ -0,0 +1,496 @@ +import { + Box, + Card, + Checkbox, + Chip, + FormControl, + FormHelperText, + Grid, + Input, + List, + ListItem, + Option, + Radio, + RadioGroup, + Select, + Typography, +} from '@mui/joy' +import { useContext, useState } from 'react' +import { UserContext } from '../../contexts/UserContext' +import { isPlusAccount } from '../../utils/Helpers' +import ThingTriggerSection from './ThingTriggerSection' + +const FREQUANCY_TYPES_RADIOS = [ + 'daily', + 'weekly', + 'monthly', + 'yearly', + 'adaptive', + 'custom', +] + +const FREQUENCY_TYPE_MESSAGE = { + adaptive: + 'This chore will be scheduled dynamically based on previous completion dates.', + custom: 'This chore will be scheduled based on a custom frequency.', +} +const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month'] +const FREQUANCY_TYPES = [ + 'once', + 'daily', + 'weekly', + 'monthly', + 'yearly', + 'adaptive', + ...REPEAT_ON_TYPE, +] +const MONTH_WITH_NO_31_DAYS = [ + // TODO: Handle these months if day is 31 + 'february', + 'april', + 'june', + 'september', + 'november', +] +const RepeatOnSections = ({ + frequencyType, + frequency, + onFrequencyUpdate, + onFrequencyTypeUpdate, + frequencyMetadata, + onFrequencyMetadataUpdate, + things, +}) => { + const [months, setMonths] = useState({}) + // const [dayOftheMonth, setDayOftheMonth] = useState(1) + const [daysOfTheWeek, setDaysOfTheWeek] = useState({}) + const [monthsOfTheYear, setMonthsOfTheYear] = useState({}) + const [intervalUnit, setIntervalUnit] = useState('days') + + switch (frequencyType) { + case 'interval': + return ( + <Grid item sm={12} sx={{ display: 'flex', alignItems: 'center' }}> + <Typography level='h5'>Every: </Typography> + <Input + type='number' + value={frequency} + onChange={e => { + if (e.target.value < 1) { + e.target.value = 1 + } + onFrequencyUpdate(e.target.value) + }} + /> + <Select placeholder='Unit' value={intervalUnit}> + {['hours', 'days', 'weeks', 'months', 'years'].map(item => ( + <Option + key={item} + value={item} + onClick={() => { + setIntervalUnit(item) + onFrequencyMetadataUpdate({ + unit: item, + }) + }} + > + {item.charAt(0).toUpperCase() + item.slice(1)} + </Option> + ))} + </Select> + </Grid> + ) + case 'days_of_the_week': + return ( + <Grid item sm={12} sx={{ display: 'flex', alignItems: 'center' }}> + <Card> + <List + orientation='horizontal' + wrap + sx={{ + '--List-gap': '8px', + '--ListItem-radius': '20px', + }} + > + {[ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ].map(item => ( + <ListItem key={item}> + <Checkbox + // disabled={index === 0} + + checked={frequencyMetadata?.days?.includes(item) || false} + onClick={() => { + const newDaysOfTheWeek = frequencyMetadata['days'] || [] + if (newDaysOfTheWeek.includes(item)) { + newDaysOfTheWeek.splice( + newDaysOfTheWeek.indexOf(item), + 1, + ) + } else { + newDaysOfTheWeek.push(item) + } + + onFrequencyMetadataUpdate({ + days: newDaysOfTheWeek.sort(), + }) + }} + overlay + disableIcon + variant='soft' + label={item.charAt(0).toUpperCase() + item.slice(1)} + /> + </ListItem> + ))} + </List> + </Card> + </Grid> + ) + case 'day_of_the_month': + return ( + <Grid + item + sm={12} + sx={{ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'space-between', + }} + > + <Box + sx={{ + display: 'flex', + alignItems: 'center', + mb: 1.5, + }} + > + <Typography>on the </Typography> + <Input + sx={{ width: '80px' }} + type='number' + value={frequency} + onChange={e => { + if (e.target.value < 1) { + e.target.value = 1 + } else if (e.target.value > 31) { + e.target.value = 31 + } + // setDayOftheMonth(e.target.value) + + onFrequencyUpdate(e.target.value) + }} + /> + <Typography>of the following month/s: </Typography> + </Box> + <Card> + <List + orientation='horizontal' + wrap + sx={{ + '--List-gap': '8px', + '--ListItem-radius': '20px', + }} + > + {[ + 'january', + 'february', + 'march', + 'april', + 'may', + 'june', + 'july', + 'august', + 'september', + 'october', + 'november', + 'december', + ].map(item => ( + <ListItem key={item}> + <Checkbox + // disabled={index === 0} + checked={frequencyMetadata?.months?.includes(item)} + // checked={months[item] || false} + // onClick={() => { + // const newMonthsOfTheYear = { + // ...monthsOfTheYear, + // } + // newMonthsOfTheYear[item] = !newMonthsOfTheYear[item] + // onFrequencyMetadataUpdate({ + // months: newMonthsOfTheYear, + // }) + // setMonthsOfTheYear(newMonthsOfTheYear) + // }} + onClick={() => { + const newMonthsOfTheYear = + frequencyMetadata['months'] || [] + if (newMonthsOfTheYear.includes(item)) { + newMonthsOfTheYear.splice( + newMonthsOfTheYear.indexOf(item), + 1, + ) + } else { + newMonthsOfTheYear.push(item) + } + + onFrequencyMetadataUpdate({ + months: newMonthsOfTheYear.sort(), + }) + console.log('newMonthsOfTheYear', newMonthsOfTheYear) + // setDaysOfTheWeek(newDaysOfTheWeek) + }} + overlay + disableIcon + variant='soft' + label={item.charAt(0).toUpperCase() + item.slice(1)} + /> + </ListItem> + ))} + </List> + </Card> + </Grid> + ) + + default: + return <></> + } +} + +const RepeatSection = ({ + frequencyType, + frequency, + onFrequencyUpdate, + onFrequencyTypeUpdate, + frequencyMetadata, + onFrequencyMetadataUpdate, + frequencyError, + allUserThings, + onTriggerUpdate, + OnTriggerValidate, + isAttemptToSave, + selectedThing, +}) => { + const [repeatOn, setRepeatOn] = useState('interval') + const { userProfile, setUserProfile } = useContext(UserContext) + return ( + <Box mt={2}> + <Typography level='h4'>Repeat :</Typography> + <FormControl sx={{ mt: 1 }}> + <Checkbox + onChange={e => { + onFrequencyTypeUpdate(e.target.checked ? 'daily' : 'once') + if (e.target.checked) { + onTriggerUpdate(null) + } + }} + defaultChecked={!['once', 'trigger'].includes(frequencyType)} + checked={!['once', 'trigger'].includes(frequencyType)} + value={!['once', 'trigger'].includes(frequencyType)} + overlay + label='Repeat this task' + /> + <FormHelperText> + Is this something needed to be done regularly? + </FormHelperText> + </FormControl> + {!['once', 'trigger'].includes(frequencyType) && ( + <> + <Card sx={{ mt: 1 }}> + <Typography level='h5'>How often should it be repeated?</Typography> + + <List + orientation='horizontal' + wrap + sx={{ + '--List-gap': '8px', + '--ListItem-radius': '20px', + }} + > + {FREQUANCY_TYPES_RADIOS.map((item, index) => ( + <ListItem key={item}> + <Checkbox + // disabled={index === 0} + checked={ + item === frequencyType || + (item === 'custom' && + REPEAT_ON_TYPE.includes(frequencyType)) + } + // defaultChecked={item === frequencyType} + onClick={() => { + if (item === 'custom') { + onFrequencyTypeUpdate(REPEAT_ON_TYPE[0]) + onFrequencyUpdate(1) + onFrequencyMetadataUpdate({ + unit: 'days', + }) + return + } + onFrequencyTypeUpdate(item) + }} + overlay + disableIcon + variant='soft' + label={ + item.charAt(0).toUpperCase() + + item.slice(1).replace('_', ' ') + } + /> + </ListItem> + ))} + </List> + <Typography>{FREQUENCY_TYPE_MESSAGE[frequencyType]}</Typography> + {frequencyType === 'custom' || + (REPEAT_ON_TYPE.includes(frequencyType) && ( + <> + <Grid container spacing={1} mt={2}> + <Grid item> + <Typography>Repeat on:</Typography> + <Box + sx={{ display: 'flex', alignItems: 'center', gap: 2 }} + > + <RadioGroup + orientation='horizontal' + aria-labelledby='segmented-controls-example' + name='justify' + // value={justify} + // onChange={event => setJustify(event.target.value)} + sx={{ + minHeight: 48, + padding: '4px', + borderRadius: '12px', + bgcolor: 'neutral.softBg', + '--RadioGroup-gap': '4px', + '--Radio-actionRadius': '8px', + mb: 1, + }} + > + {REPEAT_ON_TYPE.map(item => ( + <Radio + key={item} + color='neutral' + checked={item === frequencyType} + onClick={() => { + if ( + item === 'day_of_the_month' || + item === 'interval' + ) { + onFrequencyUpdate(1) + } + onFrequencyTypeUpdate(item) + if (item === 'days_of_the_week') { + onFrequencyMetadataUpdate({ days: [] }) + } else if (item === 'day_of_the_month') { + onFrequencyMetadataUpdate({ months: [] }) + } else if (item === 'interval') { + onFrequencyMetadataUpdate({ unit: 'days' }) + } + // setRepeatOn(item) + }} + value={item} + disableIcon + label={item + .split('_') + .map((i, idx) => { + // first or last word + if ( + idx === 0 || + idx === item.split('_').length - 1 + ) { + return ( + i.charAt(0).toUpperCase() + i.slice(1) + ) + } + return i + }) + .join(' ')} + variant='plain' + sx={{ + px: 2, + alignItems: 'center', + }} + slotProps={{ + action: ({ checked }) => ({ + sx: { + ...(checked && { + bgcolor: 'background.surface', + boxShadow: 'sm', + '&:hover': { + bgcolor: 'background.surface', + }, + }), + }, + }), + }} + /> + ))} + </RadioGroup> + </Box> + </Grid> + + <RepeatOnSections + frequency={frequency} + onFrequencyUpdate={onFrequencyUpdate} + frequencyType={frequencyType} + onFrequencyTypeUpdate={onFrequencyTypeUpdate} + frequencyMetadata={frequencyMetadata || {}} + onFrequencyMetadataUpdate={onFrequencyMetadataUpdate} + things={allUserThings} + /> + </Grid> + </> + ))} + <FormControl error={Boolean(frequencyError)}> + <FormHelperText error>{frequencyError}</FormHelperText> + </FormControl> + </Card> + </> + )} + <FormControl sx={{ mt: 1 }}> + <Checkbox + onChange={e => { + onFrequencyTypeUpdate(e.target.checked ? 'trigger' : 'once') + // if unchecked, set selectedThing to null: + if (!e.target.checked) { + onTriggerUpdate(null) + } + }} + defaultChecked={frequencyType === 'trigger'} + checked={frequencyType === 'trigger'} + value={frequencyType === 'trigger'} + disabled={!isPlusAccount(userProfile)} + overlay + label='Trigger this task based on a thing state' + /> + <FormHelperText + sx={{ + opacity: !isPlusAccount(userProfile) ? 0.5 : 1, + }} + > + Is this something that should be done when a thing state changes?{' '} + {!isPlusAccount(userProfile) && ( + <Chip variant='soft' color='warning'> + Not available in Basic Plan + </Chip> + )} + </FormHelperText> + </FormControl> + {frequencyType === 'trigger' && ( + <ThingTriggerSection + things={allUserThings} + onTriggerUpdate={onTriggerUpdate} + onValidate={OnTriggerValidate} + isAttemptToSave={isAttemptToSave} + selected={selectedThing} + /> + )} + </Box> + ) +} + +export default RepeatSection diff --git a/src/views/ChoreEdit/ThingTriggerSection.jsx b/src/views/ChoreEdit/ThingTriggerSection.jsx new file mode 100644 index 0000000..7a040ad --- /dev/null +++ b/src/views/ChoreEdit/ThingTriggerSection.jsx @@ -0,0 +1,230 @@ +import { Widgets } from '@mui/icons-material' +import { + Autocomplete, + Box, + Button, + Card, + Chip, + FormControl, + FormLabel, + Input, + ListItem, + ListItemContent, + ListItemDecorator, + Option, + Select, + TextField, + Typography, +} from '@mui/joy' +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +const isValidTrigger = (thing, condition, triggerState) => { + const newErrors = {} + if (!thing || !triggerState) { + newErrors.thing = 'Please select a thing and trigger state' + return false + } + if (thing.type === 'boolean') { + if (['true', 'false'].includes(triggerState)) { + return true + } else { + newErrors.type = 'Boolean type does not require a condition' + return false + } + } + if (thing.type === 'number') { + if (isNaN(triggerState)) { + newErrors.triggerState = 'Trigger state must be a number' + return false + } + if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(condition)) { + return true + } + } + if (thing.type === 'text') { + if (typeof triggerState === 'string') { + return true + } + } + newErrors.triggerState = 'Trigger state must be a number' + + return false +} + +const ThingTriggerSection = ({ + things, + onTriggerUpdate, + onValidate, + selected, + isAttepmtingToSave, +}) => { + const [selectedThing, setSelectedThing] = useState(null) + const [condition, setCondition] = useState(null) + const [triggerState, setTriggerState] = useState(null) + const navigate = useNavigate() + + useEffect(() => { + if (selected) { + setSelectedThing(things?.find(t => t.id === selected.thingId)) + setCondition(selected.condition) + setTriggerState(selected.triggerState) + } + }, [things]) + + useEffect(() => { + if (selectedThing && triggerState) { + onTriggerUpdate({ + thing: selectedThing, + condition: condition, + triggerState: triggerState, + }) + } + if (isValidTrigger(selectedThing, condition, triggerState)) { + onValidate(true) + } else { + onValidate(false) + } + }, [selectedThing, condition, triggerState]) + + return ( + <Card sx={{ mt: 1 }}> + <Typography level='h5'> + Trigger a task when a thing state changes to a desired state + </Typography> + {things.length !== 0 && ( + <Typography level='body-sm'> + it's look like you don't have any things yet, create a thing to + trigger a task when the state changes. + <Button + startDecorator={<Widgets />} + size='sm' + onClick={() => { + navigate('/things') + }} + > + Go to Things + </Button>{' '} + to create a thing + </Typography> + )} + <FormControl error={isAttepmtingToSave && !selectedThing}> + <Autocomplete + options={things} + value={selectedThing} + onChange={(e, newValue) => setSelectedThing(newValue)} + getOptionLabel={option => option.name} + renderOption={(props, option) => ( + <ListItem {...props}> + <Box + sx={{ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'center', + p: 1, + }} + > + <ListItemDecorator sx={{ alignSelf: 'flex-start' }}> + <Typography level='body-lg' textColor='primary'> + {option.name} + </Typography> + </ListItemDecorator> + <ListItemContent> + <Typography level='body2' textColor='text.secondary'> + <Chip>type: {option.type}</Chip>{' '} + <Chip>state: {option.state}</Chip> + </Typography> + </ListItemContent> + </Box> + </ListItem> + )} + renderInput={params => ( + <TextField {...params} label='Select a thing' /> + )} + /> + </FormControl> + <Typography level='body-sm'> + Create a condition to trigger a task when the thing state changes to + desired state + </Typography> + {selectedThing?.type == 'boolean' && ( + <Box> + <Typography level='body-sm'> + When the state of {selectedThing.name} changes as specified below, + the task will become due. + </Typography> + <Select + value={triggerState} + onChange={e => { + if (e?.target.value === 'true' || e?.target.value === 'false') + setTriggerState(e.target.value) + else setTriggerState('false') + }} + > + {['true', 'false'].map(state => ( + <Option + key={state} + value={state} + onClick={() => setTriggerState(state)} + > + {state.charAt(0).toUpperCase() + state.slice(1)} + </Option> + ))} + </Select> + </Box> + )} + {selectedThing?.type == 'number' && ( + <Box> + <Typography level='body-sm'> + When the state of {selectedThing.name} changes as specified below, + the task will become due. + </Typography> + + <Box sx={{ display: 'flex', gap: 1, direction: 'row' }}> + <Typography level='body-sm'>State is</Typography> + <Select value={condition} sx={{ width: '50%' }}> + {[ + { name: 'Equal', value: 'eq' }, + { name: 'Not equal', value: 'neq' }, + { name: 'Greater than', value: 'gt' }, + { name: 'Greater than or equal', value: 'gte' }, + { name: 'Less than', value: 'lt' }, + { name: 'Less than or equal', value: 'lte' }, + ].map(condition => ( + <Option + key={condition.value} + value={condition.value} + onClick={() => setCondition(condition.value)} + > + {condition.name} + </Option> + ))} + </Select> + <Input + type='number' + value={triggerState} + onChange={e => setTriggerState(e.target.value)} + sx={{ width: '50%' }} + /> + </Box> + </Box> + )} + {selectedThing?.type == 'text' && ( + <Box> + <Typography level='body-sm'> + When the state of {selectedThing.name} changes as specified below, + the task will become due. + </Typography> + + <Input + value={triggerState} + onChange={e => setTriggerState(e.target.value)} + label='Enter the text to trigger the task' + /> + </Box> + )} + </Card> + ) +} + +export default ThingTriggerSection diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx new file mode 100644 index 0000000..0b2a408 --- /dev/null +++ b/src/views/Chores/ChoreCard.jsx @@ -0,0 +1,578 @@ +import { + Check, + Delete, + Edit, + HowToReg, + KeyboardDoubleArrowUp, + LocalOffer, + ManageSearch, + MoreTime, + MoreVert, + NoteAdd, + RecordVoiceOver, + Repeat, + Report, + SwitchAccessShortcut, + TimesOneMobiledata, + Update, + Webhook, +} from '@mui/icons-material' +import { + Avatar, + Box, + Card, + Chip, + CircularProgress, + Divider, + Grid, + IconButton, + Menu, + MenuItem, + Typography, +} from '@mui/joy' +import moment from 'moment' +import React, { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { API_URL } from '../../Config' +import { Fetch } from '../../utils/TokenManager' +import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import DateModal from '../Modals/Inputs/DateModal' +import SelectModal from '../Modals/Inputs/SelectModal' +import TextModal from '../Modals/Inputs/TextModal' +const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => { + const [activeUserId, setActiveUserId] = React.useState(0) + const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] = + React.useState(false) + const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] = + React.useState(false) + const [isChangeAssigneeModalOpen, setIsChangeAssigneeModalOpen] = + React.useState(false) + const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] = + React.useState(false) + const [confirmModelConfig, setConfirmModelConfig] = React.useState({}) + const [anchorEl, setAnchorEl] = React.useState(null) + const menuRef = React.useRef(null) + const navigate = useNavigate() + const [isDisabled, setIsDisabled] = React.useState(false) + + // useEffect(() => { + // GetAllUsers() + // .then(response => response.json()) + // .then(data => { + // setPerformers(data.res) + // }) + // }, []) + + useEffect(() => { + document.addEventListener('mousedown', handleMenuOutsideClick) + return () => { + document.removeEventListener('mousedown', handleMenuOutsideClick) + } + }, [anchorEl]) + + const handleMenuOpen = event => { + setAnchorEl(event.currentTarget) + } + + const handleMenuClose = () => { + setAnchorEl(null) + } + + const handleMenuOutsideClick = event => { + if ( + anchorEl && + !anchorEl.contains(event.target) && + !menuRef.current.contains(event.target) + ) { + handleMenuClose() + } + } + const handleEdit = () => { + navigate(`/chores/${chore.id}/edit`) + } + const handleDelete = () => { + setConfirmModelConfig({ + isOpen: true, + title: 'Delete Chore', + confirmText: 'Delete', + cancelText: 'Cancel', + message: 'Are you sure you want to delete this chore?', + onClose: isConfirmed => { + console.log('isConfirmed', isConfirmed) + if (isConfirmed === true) { + Fetch(`${API_URL}/chores/${chore.id}`, { + method: 'DELETE', + }).then(response => { + if (response.ok) { + onChoreRemove(chore) + } + }) + } + setConfirmModelConfig({}) + }, + }) + } + + const handleCompleteChore = () => { + Fetch(`${API_URL}/chores/${chore.id}/do`, { + method: 'POST', + }).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + onChoreUpdate(newChore, 'completed') + }) + } + }) + setIsDisabled(true) + setTimeout(() => setIsDisabled(false), 5000) // Re-enable the button after 5 seconds + } + const handleChangeDueDate = newDate => { + if (activeUserId === null) { + alert('Please select a performer') + return + } + Fetch(`${API_URL}/chores/${chore.id}/dueDate`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + dueDate: newDate ? new Date(newDate).toISOString() : null, + UpdatedBy: activeUserId, + }), + }).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + onChoreUpdate(newChore, 'rescheduled') + }) + } + }) + } + + const handleCompleteWithPastDate = newDate => { + if (activeUserId === null) { + alert('Please select a performer') + return + } + Fetch( + `${API_URL}/chores/${chore.id}/do?completedDate=${new Date( + newDate, + ).toISOString()}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }, + ).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + onChoreUpdate(newChore, 'completed') + }) + } + }) + } + const handleAssigneChange = assigneeId => { + // TODO: Implement assignee change + } + const handleCompleteWithNote = note => { + Fetch(`${API_URL}/chores/${chore.id}/do`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + note: note, + }), + }).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + onChoreUpdate(newChore, 'completed') + }) + } + }) + } + const getDueDateChipText = nextDueDate => { + if (chore.nextDueDate === null) return 'No Due Date' + // if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM + const diff = moment(nextDueDate).diff(moment(), 'hours') + if (diff < 48 && diff > 0) { + return moment(nextDueDate).calendar().replace(' at', '') + } + return 'Due ' + moment(nextDueDate).fromNow() + } + const getDueDateChipColor = nextDueDate => { + if (chore.nextDueDate === null) return 'neutral' + const diff = moment(nextDueDate).diff(moment(), 'hours') + if (diff < 48 && diff > 0) { + return 'warning' + } + if (diff < 0) { + return 'danger' + } + + return 'neutral' + } + + const getIconForLabel = label => { + if (!label || label.trim() === '') return <></> + switch (String(label).toLowerCase()) { + case 'high': + return <KeyboardDoubleArrowUp /> + case 'important': + return <Report /> + default: + return <LocalOffer /> + } + } + + const getRecurrentChipText = chore => { + const dayOfMonthSuffix = n => { + if (n >= 11 && n <= 13) { + return 'th' + } + switch (n % 10) { + case 1: + return 'st' + case 2: + return 'nd' + case 3: + return 'rd' + default: + return 'th' + } + } + if (chore.frequencyType === 'once') { + return 'Once' + } else if (chore.frequencyType === 'trigger') { + return 'Trigger' + } else if (chore.frequencyType === 'daily') { + return 'Daily' + } else if (chore.frequencyType === 'weekly') { + return 'Weekly' + } else if (chore.frequencyType === 'monthly') { + return 'Monthly' + } else if (chore.frequencyType === 'yearly') { + return 'Yearly' + } else if (chore.frequencyType === 'days_of_the_week') { + let days = JSON.parse(chore.frequencyMetadata).days + days = days.map(d => moment().day(d).format('ddd')) + return days.join(', ') + } else if (chore.frequencyType === 'day_of_the_month') { + let freqData = JSON.parse(chore.frequencyMetadata) + const months = freqData.months.map(m => moment().month(m).format('MMM')) + return `${chore.frequency}${dayOfMonthSuffix( + chore.frequency, + )} of ${months.join(', ')}` + } else if (chore.frequencyType === 'interval') { + return `Every ${chore.frequency} ${ + JSON.parse(chore.frequencyMetadata).unit + }` + } else { + return chore.frequencyType + } + } + + const getFrequencyIcon = chore => { + if (['once', 'no_repeat'].includes(chore.frequencyType)) { + return <TimesOneMobiledata /> + } else if (chore.frequencyType === 'trigger') { + return <Webhook /> + } else { + return <Repeat /> + } + } + + return ( + <> + <Chip + variant='soft' + sx={{ + position: 'relative', + top: 10, + zIndex: 1, + left: 10, + }} + color={getDueDateChipColor(chore.nextDueDate)} + > + {getDueDateChipText(chore.nextDueDate)} + </Chip> + + <Chip + variant='soft' + sx={{ + position: 'relative', + top: 10, + zIndex: 1, + ml: 0.4, + left: 10, + }} + > + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }} + > + {getFrequencyIcon(chore)} + + {getRecurrentChipText(chore)} + </div> + </Chip> + + <Card + variant='plain' + sx={{ + ...sx, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + p: 2, + // backgroundColor: 'white', + boxShadow: 'sm', + borderRadius: 20, + + // mb: 2, + }} + > + <Grid container> + <Grid item xs={9}> + {/* Box in top right with Chip showing next due date */} + <Box display='flex' justifyContent='start' alignItems='center'> + <Avatar sx={{ mr: 1, fontSize: 22 }}> + {chore.name.charAt(0).toUpperCase()} + </Avatar> + <Box display='flex' flexDirection='column'> + <Typography level='title-md'>{chore.name}</Typography> + <Typography level='body-md' color='text.disabled'> + Assigned to{' '} + <Chip variant='outlined'> + { + performers.find(p => p.id === chore.assignedTo) + ?.displayName + } + </Chip> + </Typography> + <Box> + {chore.labels?.split(',').map(label => ( + <Chip + variant='solid' + key={label} + color='primary' + sx={{ + position: 'relative', + ml: 0.5, + top: 10, + zIndex: 1, + left: 10, + }} + startDecorator={getIconForLabel(label)} + > + {label} + </Chip> + ))} + </Box> + </Box> + </Box> + {/* <Box display='flex' justifyContent='space-between' alignItems='center'> + <Chip variant='outlined'> + {chore.nextDueDate === null + ? '--' + : 'Due ' + moment(chore.nextDueDate).fromNow()} + </Chip> + </Box> */} + </Grid> + <Grid + item + xs={3} + sx={{ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }} + > + <Box display='flex' justifyContent='flex-end' alignItems='flex-end'> + {/* <ButtonGroup> */} + <IconButton + variant='solid' + color='success' + onClick={handleCompleteChore} + disabled={isDisabled} + sx={{ + borderRadius: '50%', + width: 50, + height: 50, + zIndex: 1, + }} + > + <div className='relative grid place-items-center'> + <Check /> + {isDisabled && ( + <CircularProgress + variant='solid' + color='success' + size='md' + sx={{ + color: 'success.main', + position: 'absolute', + zIndex: 0, + }} + /> + )} + </div> + </IconButton> + <IconButton + // sx={{ width: 15 }} + variant='soft' + color='success' + onClick={handleMenuOpen} + sx={{ + borderRadius: '50%', + width: 25, + height: 25, + position: 'relative', + left: -10, + }} + > + <MoreVert /> + </IconButton> + {/* </ButtonGroup> */} + <Menu + size='md' + ref={menuRef} + anchorEl={anchorEl} + open={Boolean(anchorEl)} + onClose={handleMenuClose} + > + <MenuItem + onClick={() => { + setIsCompleteWithNoteModalOpen(true) + }} + > + <NoteAdd /> + Complete with note + </MenuItem> + <MenuItem + onClick={() => { + setIsCompleteWithPastDateModalOpen(true) + }} + > + <Update /> + Complete in past + </MenuItem> + <MenuItem + onClick={() => { + Fetch(`${API_URL}/chores/${chore.id}/skip`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + onChoreUpdate(newChore, 'skipped') + handleMenuClose() + }) + } + }) + }} + > + <SwitchAccessShortcut /> + Skip to next due date + </MenuItem> + <MenuItem + onClick={() => { + setIsChangeAssigneeModalOpen(true) + }} + > + <RecordVoiceOver /> + Delegate to someone else + </MenuItem> + <MenuItem> + <HowToReg /> + Complete as someone else + </MenuItem> + <Divider /> + <MenuItem + onClick={() => { + navigate(`/chores/${chore.id}/history`) + }} + > + <ManageSearch /> + History + </MenuItem> + <Divider /> + <MenuItem + onClick={() => { + setIsChangeDueDateModalOpen(true) + }} + > + <MoreTime /> + Change due date + </MenuItem> + <MenuItem onClick={handleEdit}> + <Edit /> + Edit + </MenuItem> + <MenuItem onClick={handleDelete} color='danger'> + <Delete /> + Delete + </MenuItem> + </Menu> + </Box> + </Grid> + </Grid> + <DateModal + isOpen={isChangeDueDateModalOpen} + key={'changeDueDate' + chore.id} + current={chore.nextDueDate} + title={`Change due date`} + onClose={() => { + setIsChangeDueDateModalOpen(false) + }} + onSave={handleChangeDueDate} + /> + <DateModal + isOpen={isCompleteWithPastDateModalOpen} + key={'completedInPast' + chore.id} + current={chore.nextDueDate} + title={`Save Chore that you completed in the past`} + onClose={() => { + setIsCompleteWithPastDateModalOpen(false) + }} + onSave={handleCompleteWithPastDate} + /> + <SelectModal + isOpen={isChangeAssigneeModalOpen} + options={performers} + displayKey='displayName' + title={`Delegate to someone else`} + onClose={() => { + setIsChangeAssigneeModalOpen(false) + }} + onSave={handleAssigneChange} + /> + <ConfirmationModal config={confirmModelConfig} /> + <TextModal + isOpen={isCompleteWithNoteModalOpen} + title='Add note to attach to this completion:' + onClose={() => { + setIsCompleteWithNoteModalOpen(false) + }} + okText={'Complete'} + onSave={handleCompleteWithNote} + /> + </Card> + </> + ) +} + +export default ChoreCard diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx new file mode 100644 index 0000000..98fd443 --- /dev/null +++ b/src/views/Chores/MyChores.jsx @@ -0,0 +1,384 @@ +import { Add, EditCalendar } from '@mui/icons-material' +import { + Badge, + Box, + Checkbox, + CircularProgress, + Container, + IconButton, + List, + ListItem, + Menu, + MenuItem, + Snackbar, + Typography, +} from '@mui/joy' +import { useContext, useEffect, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { UserContext } from '../../contexts/UserContext' +import Logo from '../../Logo' +import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher' +import ChoreCard from './ChoreCard' + +const MyChores = () => { + const { userProfile, setUserProfile } = useContext(UserContext) + const [isSnackbarOpen, setIsSnackbarOpen] = useState(false) + const [snackBarMessage, setSnackBarMessage] = useState(null) + const [chores, setChores] = useState([]) + const [filteredChores, setFilteredChores] = useState([]) + const [selectedFilter, setSelectedFilter] = useState('All') + const [activeUserId, setActiveUserId] = useState(0) + const [performers, setPerformers] = useState([]) + const [anchorEl, setAnchorEl] = useState(null) + const menuRef = useRef(null) + const Navigate = useNavigate() + const choreSorter = (a, b) => { + // 1. Handle null due dates (always last): + if (!a.nextDueDate && !b.nextDueDate) return 0 // Both null, no order + if (!a.nextDueDate) return 1 // a is null, comes later + if (!b.nextDueDate) return -1 // b is null, comes earlier + + const aDueDate = new Date(a.nextDueDate) + const bDueDate = new Date(b.nextDueDate) + const now = new Date() + + const oneDayInMs = 24 * 60 * 60 * 1000 + + // 2. Prioritize tasks due today +- 1 day: + const aTodayOrNear = Math.abs(aDueDate - now) <= oneDayInMs + const bTodayOrNear = Math.abs(bDueDate - now) <= oneDayInMs + if (aTodayOrNear && !bTodayOrNear) return -1 // a is closer + if (!aTodayOrNear && bTodayOrNear) return 1 // b is closer + + // 3. Handle overdue tasks (excluding today +- 1): + const aOverdue = aDueDate < now && !aTodayOrNear + const bOverdue = bDueDate < now && !bTodayOrNear + if (aOverdue && !bOverdue) return -1 // a is overdue, comes earlier + if (!aOverdue && bOverdue) return 1 // b is overdue, comes earlier + + // 4. Sort future tasks by due date: + return aDueDate - bDueDate // Sort ascending by due date + } + + const handleSelectedFilter = selected => { + setFilteredChores(FILTERS[selected](chores)) + + setSelectedFilter(selected) + } + + useEffect(() => { + if (userProfile === null) { + GetUserProfile() + .then(response => response.json()) + .then(data => { + setUserProfile(data.res) + }) + } + GetChores() + .then(response => response.json()) + .then(data => { + data.res.sort(choreSorter) + setChores(data.res) + + setFilteredChores(data.res) + }) + + GetAllUsers() + .then(response => response.json()) + .then(data => { + setPerformers(data.res) + }) + + const currentUser = JSON.parse(localStorage.getItem('user')) + if (currentUser !== null) { + setActiveUserId(currentUser.id) + } + }, []) + useEffect(() => { + document.addEventListener('mousedown', handleMenuOutsideClick) + return () => { + document.removeEventListener('mousedown', handleMenuOutsideClick) + } + }, [anchorEl]) + const handleMenuOutsideClick = event => { + if ( + anchorEl && + !anchorEl.contains(event.target) && + !menuRef.current.contains(event.target) + ) { + handleFilterMenuClose() + } + } + const handleFilterMenuOpen = event => { + event.preventDefault() + setAnchorEl(event.currentTarget) + } + + const handleFilterMenuClose = () => { + setAnchorEl(null) + } + const handleChoreUpdated = (updatedChore, event) => { + const newChores = chores.map(chore => { + if (chore.id === updatedChore.id) { + return updatedChore + } + return chore + }) + + const newFilteredChores = filteredChores.map(chore => { + if (chore.id === updatedChore.id) { + return updatedChore + } + return chore + }) + setChores(newChores) + setFilteredChores(newFilteredChores) + switch (event) { + case 'completed': + setSnackBarMessage('Completed') + break + case 'skipped': + setSnackBarMessage('Skipped') + break + case 'rescheduled': + setSnackBarMessage('Rescheduled') + break + default: + setSnackBarMessage('Updated') + } + setIsSnackbarOpen(true) + } + + const handleChoreDeleted = deletedChore => { + const newChores = chores.filter(chore => chore.id !== deletedChore.id) + const newFilteredChores = filteredChores.filter( + chore => chore.id !== deletedChore.id, + ) + setChores(newChores) + setFilteredChores(newFilteredChores) + } + + if (userProfile === null) { + return ( + <Container className='flex h-full items-center justify-center'> + <Box className='flex flex-col items-center justify-center'> + <CircularProgress + color='success' + sx={{ '--CircularProgress-size': '200px' }} + > + <Logo /> + </CircularProgress> + </Box> + </Container> + ) + } + + return ( + <Container maxWidth='md'> + {/* <Typography level='h3' mb={1.5}> + My Chores + </Typography> */} + {/* <Sheet> */} + <List + orientation='horizontal' + wrap + sx={{ + '--List-gap': '8px', + '--ListItem-radius': '20px', + '--ListItem-minHeight': '32px', + '--ListItem-gap': '4px', + mt: 0.2, + }} + > + {['All', 'Overdue', 'Due today', 'Due in week'].map(filter => ( + <Badge + key={filter} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + variant='outlined' + color={selectedFilter === filter ? 'primary' : 'neutral'} + badgeContent={FILTERS[filter](chores).length} + badgeInset={'5px'} + > + <ListItem key={filter}> + <Checkbox + key={'checkbox' + filter} + label={filter} + onClick={() => handleSelectedFilter(filter)} + checked={filter === selectedFilter} + disableIcon + overlay + size='sm' + /> + </ListItem> + </Badge> + ))} + + <ListItem onClick={handleFilterMenuOpen}> + <Checkbox key='checkboxAll' label='⋮' disableIcon overlay size='lg' /> + </ListItem> + <Menu + ref={menuRef} + anchorEl={anchorEl} + open={Boolean(anchorEl)} + onClose={handleFilterMenuClose} + > + <MenuItem + onClick={() => { + setFilteredChores( + FILTERS['Assigned To Me'](chores, userProfile.id), + ) + setSelectedFilter('Assigned To Me') + handleFilterMenuClose() + }} + > + Assigned to me + </MenuItem> + <MenuItem + onClick={() => { + setFilteredChores( + FILTERS['Created By Me'](chores, userProfile.id), + ) + setSelectedFilter('Created By Me') + handleFilterMenuClose() + }} + > + Created by me + </MenuItem> + <MenuItem + onClick={() => { + setFilteredChores(FILTERS['No Due Date'](chores, userProfile.id)) + setSelectedFilter('No Due Date') + handleFilterMenuClose() + }} + > + No Due Date + </MenuItem> + </Menu> + </List> + {/* </Sheet> */} + {filteredChores.length === 0 && ( + <Box + sx={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + height: '50vh', + }} + > + <EditCalendar + sx={{ + fontSize: '4rem', + // color: 'text.disabled', + mb: 1, + }} + /> + <Typography level='title-md' gutterBottom> + Nothing scheduled + </Typography> + </Box> + )} + + {filteredChores.map(chore => ( + <ChoreCard + key={chore.id} + chore={chore} + onChoreUpdate={handleChoreUpdated} + onChoreRemove={handleChoreDeleted} + performers={performers} + /> + ))} + + <Box + // variant='outlined' + sx={{ + position: 'fixed', + bottom: 0, + left: 10, + p: 2, // padding + display: 'flex', + justifyContent: 'flex-end', + gap: 2, + 'z-index': 1000, + }} + > + <IconButton + color='primary' + variant='solid' + sx={{ + borderRadius: '50%', + width: 50, + height: 50, + }} + // startDecorator={<Add />} + onClick={() => { + Navigate(`/chores/create`) + }} + > + <Add /> + </IconButton> + </Box> + <Snackbar + open={isSnackbarOpen} + onClose={() => { + setIsSnackbarOpen(false) + }} + autoHideDuration={3000} + variant='soft' + color='success' + size='lg' + invertedColors + > + <Typography level='title-md'>{snackBarMessage}</Typography> + </Snackbar> + </Container> + ) +} + +const FILTERS = { + All: function (chores) { + return chores + }, + Overdue: function (chores) { + return chores.filter(chore => { + if (chore.nextDueDate === null) return false + return new Date(chore.nextDueDate) < new Date() + }) + }, + 'Due today': function (chores) { + return chores.filter(chore => { + return ( + new Date(chore.nextDueDate).toDateString() === new Date().toDateString() + ) + }) + }, + 'Due in week': function (chores) { + return chores.filter(chore => { + return ( + new Date(chore.nextDueDate) < + new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) && + new Date(chore.nextDueDate) > new Date() + ) + }) + }, + 'Created By Me': function (chores, userID) { + return chores.filter(chore => { + return chore.createdBy === userID + }) + }, + 'Assigned To Me': function (chores, userID) { + return chores.filter(chore => { + return chore.assignedTo === userID + }) + }, + 'No Due Date': function (chores, userID) { + return chores.filter(chore => { + return chore.nextDueDate === null + }) + }, +} + +export default MyChores diff --git a/src/views/ChoresOverview.jsx b/src/views/ChoresOverview.jsx new file mode 100644 index 0000000..396ab0d --- /dev/null +++ b/src/views/ChoresOverview.jsx @@ -0,0 +1,354 @@ +import { + Adjust, + CancelRounded, + CheckBox, + Edit, + HelpOutline, + History, + QueryBuilder, + SearchRounded, + Warning, +} from '@mui/icons-material' +import { + Avatar, + Button, + ButtonGroup, + Chip, + Container, + Grid, + IconButton, + Input, + Table, + Tooltip, + Typography, +} from '@mui/joy' + +import moment from 'moment' +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { API_URL } from '../Config' +import { GetAllUsers } from '../utils/Fetcher' +import { Fetch } from '../utils/TokenManager' +import DateModal from './Modals/Inputs/DateModal' +// import moment from 'moment' + +// enum for chore status: +const CHORE_STATUS = { + NO_DUE_DATE: 'No due date', + DUE_SOON: 'Soon', + DUE_NOW: 'Due', + OVER_DUE: 'Overdue', +} + +const ChoresOverview = () => { + const [chores, setChores] = useState([]) + const [filteredChores, setFilteredChores] = useState([]) + const [performers, setPerformers] = useState([]) + const [activeUserId, setActiveUserId] = useState(null) + const [isDateModalOpen, setIsDateModalOpen] = useState(false) + const [choreId, setChoreId] = useState(null) + const [search, setSearch] = useState('') + const Navigate = useNavigate() + + const getChoreStatus = chore => { + if (chore.nextDueDate === null) { + return CHORE_STATUS.NO_DUE_DATE + } + const dueDate = new Date(chore.nextDueDate) + const now = new Date() + const diff = dueDate - now + if (diff < 0) { + return CHORE_STATUS.OVER_DUE + } + if (diff > 1000 * 60 * 60 * 24) { + return CHORE_STATUS.DUE_NOW + } + if (diff > 0) { + return CHORE_STATUS.DUE_SOON + } + return CHORE_STATUS.NO_DUE_DATE + } + const getChoreStatusColor = chore => { + switch (getChoreStatus(chore)) { + case CHORE_STATUS.NO_DUE_DATE: + return 'neutral' + case CHORE_STATUS.DUE_SOON: + return 'success' + case CHORE_STATUS.DUE_NOW: + return 'primary' + case CHORE_STATUS.OVER_DUE: + return 'warning' + default: + return 'neutral' + } + } + const getChoreStatusIcon = chore => { + switch (getChoreStatus(chore)) { + case CHORE_STATUS.NO_DUE_DATE: + return <HelpOutline /> + case CHORE_STATUS.DUE_SOON: + return <QueryBuilder /> + case CHORE_STATUS.DUE_NOW: + return <Adjust /> + case CHORE_STATUS.OVER_DUE: + return <Warning /> + default: + return <HelpOutline /> + } + } + useEffect(() => { + // fetch chores: + Fetch(`${API_URL}/chores/`) + .then(response => response.json()) + .then(data => { + const filteredData = data.res.filter( + chore => chore.assignedTo === activeUserId || chore.assignedTo === 0, + ) + setChores(data.res) + setFilteredChores(data.res) + }) + GetAllUsers() + .then(response => response.json()) + .then(data => { + setPerformers(data.res) + }) + const user = JSON.parse(localStorage.getItem('user')) + if (user != null && user.id > 0) { + setActiveUserId(user.id) + } + }, []) + + return ( + <Container> + <Typography level='h4' mb={1.5}> + Chores Overviews + </Typography> + {/* <SummaryCard /> */} + <Grid container> + <Grid + item + sm={6} + alignSelf={'flex-start'} + minWidth={100} + display='flex' + gap={2} + > + <Input + placeholder='Search' + value={search} + onChange={e => { + if (e.target.value === '') { + setFilteredChores(chores) + } + setSearch(e.target.value) + const newChores = chores.filter(chore => { + return chore.name.includes(e.target.value) + }) + setFilteredChores(newChores) + }} + endDecorator={ + search !== '' ? ( + <Button + variant='text' + onClick={() => { + setSearch('') + setFilteredChores(chores) + }} + > + <CancelRounded /> + </Button> + ) : ( + <Button variant='text'> + <SearchRounded /> + </Button> + ) + } + ></Input> + </Grid> + <Grid item sm={6} justifyContent={'flex-end'} display={'flex'} gap={2}> + <Button + onClick={() => { + Navigate(`/chores/create`) + }} + > + New Chore + </Button> + </Grid> + </Grid> + + <Table> + <thead> + <tr> + {/* first column has minium size because its icon */} + <th style={{ width: 100 }}>Due</th> + <th>Chore</th> + <th>Assignee</th> + <th>Due</th> + <th>Action</th> + </tr> + </thead> + <tbody> + {filteredChores.map(chore => ( + <tr key={chore.id}> + {/* cirular icon if the chore is due will be red else yellow: */} + <td> + <Chip color={getChoreStatusColor(chore)}> + {getChoreStatus(chore)} + </Chip> + </td> + <td + onClick={() => { + Navigate(`/chores/${chore.id}/edit`) + }} + > + {chore.name || '--'} + </td> + <td> + {chore.assignedTo > 0 ? ( + <Tooltip + title={ + performers.find(p => p.id === chore.assignedTo) + ?.displayName + } + size='sm' + > + <Chip + startDecorator={ + <Avatar color='primary'> + { + performers.find(p => p.id === chore.assignedTo) + ?.displayName[0] + } + </Avatar> + } + > + {performers.find(p => p.id === chore.assignedTo)?.name} + </Chip> + </Tooltip> + ) : ( + <Chip + color='warning' + startDecorator={<Avatar color='primary'>?</Avatar>} + > + Unassigned + </Chip> + )} + </td> + <td> + <Tooltip + title={ + chore.nextDueDate === null + ? 'no due date' + : moment(chore.nextDueDate).format('YYYY-MM-DD') + } + size='sm' + > + <Typography> + {chore.nextDueDate === null + ? '--' + : moment(chore.nextDueDate).fromNow()} + </Typography> + </Tooltip> + </td> + + <td> + <ButtonGroup + // display='flex' + // // justifyContent='space-around' + // alignItems={'center'} + // gap={0.5} + > + <IconButton + variant='outlined' + size='sm' + // sx={{ borderRadius: '50%' }} + onClick={() => { + Fetch(`${API_URL}/chores/${chore.id}/do`, { + method: 'POST', + }).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + const newChores = [...chores] + const index = newChores.findIndex( + c => c.id === chore.id, + ) + newChores[index] = newChore + setChores(newChores) + setFilteredChores(newChores) + }) + } + }) + }} + aria-setsize={2} + > + <CheckBox /> + </IconButton> + <IconButton + variant='outlined' + size='sm' + // sx={{ borderRadius: '50%' }} + onClick={() => { + setChoreId(chore.id) + setIsDateModalOpen(true) + }} + aria-setsize={2} + > + <History /> + </IconButton> + <IconButton + variant='outlined' + size='sm' + // sx={{ + // borderRadius: '50%', + // }} + onClick={() => { + Navigate(`/chores/${chore.id}/edit`) + }} + > + <Edit /> + </IconButton> + </ButtonGroup> + </td> + </tr> + ))} + </tbody> + </Table> + <DateModal + isOpen={isDateModalOpen} + key={choreId} + title={`Change due date`} + onClose={() => { + setIsDateModalOpen(false) + }} + onSave={date => { + if (activeUserId === null) { + alert('Please select a performer') + return + } + fetch( + `${API_URL}/chores/${choreId}/do?performer=${activeUserId}&completedDate=${new Date( + date, + ).toISOString()}`, + { + method: 'POST', + }, + ).then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + const newChores = [...chores] + const index = newChores.findIndex(c => c.id === chore.id) + newChores[index] = newChore + setChores(newChores) + setFilteredChores(newChores) + }) + } + }) + }} + /> + </Container> + ) +} + +export default ChoresOverview diff --git a/src/views/Circles/JoinCircle.jsx b/src/views/Circles/JoinCircle.jsx new file mode 100644 index 0000000..fd6d542 --- /dev/null +++ b/src/views/Circles/JoinCircle.jsx @@ -0,0 +1,154 @@ +import { Box, Container, Input, Sheet, Typography } from '@mui/joy' +import Logo from '../../Logo' + +import { Button } from '@mui/joy' +import { useContext } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { UserContext } from '../../contexts/UserContext' +import { JoinCircle } from '../../utils/Fetcher' +const JoinCircleView = () => { + const { userProfile, setUserProfile } = useContext(UserContext) + let [searchParams, setSearchParams] = useSearchParams() + const navigate = useNavigate() + const code = searchParams.get('code') + + return ( + <Container + component='main' + maxWidth='xs' + + // make content center in the middle of the page: + > + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + {/* <img + src='/src/assets/logo.svg' + alt='logo' + width='128px' + height='128px' + /> */} + <Logo /> + + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + {code && userProfile && ( + <> + <Typography level='body-md' alignSelf={'center'}> + Hi {userProfile?.displayName}, you have been invited to join the + circle{' '} + </Typography> + <Input + fullWidth + placeholder='Enter code' + value={code} + disabled={!!code} + size='lg' + sx={{ + width: '220px', + mb: 1, + }} + /> + <Typography level='body-md' alignSelf={'center'}> + Joining will give you access to the circle's chores and members. + </Typography> + <Typography level='body-md' alignSelf={'center'}> + You can leave the circle later from you Settings page. + </Typography> + <Button + fullWidth + size='lg' + sx={{ mt: 3, mb: 2 }} + onClick={() => { + JoinCircle(code).then(resp => { + if (resp.ok) { + alert( + 'Joined circle successfully, wait for the circle owner to accept your request.', + ) + navigate('/my/chores') + } else { + if (resp.status === 409) { + alert('You are already a member of this circle') + } else { + alert('Failed to join circle') + } + navigate('/my/chores') + } + }) + }} + > + Join Circle + </Button> + <Button + fullWidth + size='lg' + q + variant='plain' + sx={{ + width: '100%', + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={() => { + navigate('/my/chores') + }} + > + Cancel + </Button> + </> + )} + {!code || + (!userProfile && ( + <> + <Typography level='body-md' alignSelf={'center'}> + You need to be logged in to join a circle + </Typography> + <Typography level='body-md' alignSelf={'center'} sx={{ mb: 9 }}> + Login or sign up to continue + </Typography> + <Button + fullWidth + size='lg' + sx={{ mt: 3, mb: 2 }} + onClick={() => { + navigate('/login') + }} + > + Login + </Button> + </> + ))} + </Sheet> + </Box> + </Container> + ) +} + +export default JoinCircleView diff --git a/src/views/Error.jsx b/src/views/Error.jsx new file mode 100644 index 0000000..d04fc29 --- /dev/null +++ b/src/views/Error.jsx @@ -0,0 +1,11 @@ +import { Typography } from '@mui/joy' + +const Error = () => { + return ( + <div className='grid min-h-screen place-items-center'> + <Typography level='h1'>404</Typography> + </div> + ) +} + +export default Error diff --git a/src/views/History/BigChip.jsx b/src/views/History/BigChip.jsx new file mode 100644 index 0000000..4987367 --- /dev/null +++ b/src/views/History/BigChip.jsx @@ -0,0 +1,26 @@ +import Chip from '@mui/joy/Chip' +import * as React from 'react' + +function BigChip(props) { + return ( + <Chip + variant='outlined' + color='primary' + size='lg' // Adjust to your desired size + sx={{ + fontSize: '1rem', // Example: Increase font size + padding: '1rem', // Example: Increase padding + height: '1rem', // Adjust to your desired height + // Add other custom styles as needed + }} + {...props} + > + {props.children} + </Chip> + ) +} + +export default BigChip +BigChip.propTypes = { + ...Chip.propTypes, +} diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx new file mode 100644 index 0000000..22ea6a9 --- /dev/null +++ b/src/views/History/ChoreHistory.jsx @@ -0,0 +1,344 @@ +import { Checklist, EventBusy, Timelapse } from '@mui/icons-material' +import { + Avatar, + Box, + Button, + Chip, + CircularProgress, + Container, + Grid, + List, + ListDivider, + ListItem, + ListItemContent, + ListItemDecorator, + Sheet, + Typography, +} from '@mui/joy' +import moment from 'moment' +import React, { useEffect, useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import { API_URL } from '../../Config' +import { GetAllCircleMembers } from '../../utils/Fetcher' +import { Fetch } from '../../utils/TokenManager' + +const ChoreHistory = () => { + const [choreHistory, setChoresHistory] = useState([]) + const [userHistory, setUserHistory] = useState([]) + const [performers, setPerformers] = useState([]) + const [historyInfo, setHistoryInfo] = useState([]) + + const [isLoading, setIsLoading] = useState(true) // Add loading state + const { choreId } = useParams() + + useEffect(() => { + setIsLoading(true) // Start loading + + Promise.all([ + Fetch(`${API_URL}/chores/${choreId}/history`).then(res => res.json()), + GetAllCircleMembers().then(res => res.json()), + ]) + .then(([historyData, usersData]) => { + setChoresHistory(historyData.res) + + const newUserChoreHistory = {} + historyData.res.forEach(choreHistory => { + const userId = choreHistory.completedBy + newUserChoreHistory[userId] = (newUserChoreHistory[userId] || 0) + 1 + }) + setUserHistory(newUserChoreHistory) + + setPerformers(usersData.res) + updateHistoryInfo(historyData.res, newUserChoreHistory, usersData.res) + }) + .catch(error => { + console.error('Error fetching data:', error) + // Handle errors, e.g., show an error message to the user + }) + .finally(() => { + setIsLoading(false) // Finish loading + }) + }, [choreId]) + + const updateHistoryInfo = (histories, userHistories, performers) => { + // average delay for task completaion from due date: + + const averageDelay = + histories.reduce((acc, chore) => { + if (chore.dueDate) { + // Only consider chores with a due date + return acc + moment(chore.completedAt).diff(chore.dueDate, 'hours') + } + return acc + }, 0) / histories.length + const averageDelayMoment = moment.duration(averageDelay, 'hours') + const maximumDelay = histories.reduce((acc, chore) => { + if (chore.dueDate) { + // Only consider chores with a due date + const delay = moment(chore.completedAt).diff(chore.dueDate, 'hours') + return delay > acc ? delay : acc + } + return acc + }, 0) + + const maxDelayMoment = moment.duration(maximumDelay, 'hours') + + // find max value in userHistories: + const userCompletedByMost = Object.keys(userHistories).reduce((a, b) => + userHistories[a] > userHistories[b] ? a : b, + ) + const userCompletedByLeast = Object.keys(userHistories).reduce((a, b) => + userHistories[a] < userHistories[b] ? a : b, + ) + + const historyInfo = [ + { + icon: ( + <Avatar> + <Checklist /> + </Avatar> + ), + text: `${histories.length} completed`, + subtext: `${Object.keys(userHistories).length} users contributed`, + }, + { + icon: ( + <Avatar> + <Timelapse /> + </Avatar> + ), + text: `Completed within ${moment + .duration(averageDelayMoment) + .humanize()}`, + subtext: `Maximum delay was ${moment + .duration(maxDelayMoment) + .humanize()}`, + }, + { + icon: <Avatar></Avatar>, + text: `${ + performers.find(p => p.userId === Number(userCompletedByMost)) + ?.displayName + } completed most`, + subtext: `${userHistories[userCompletedByMost]} time/s`, + }, + ] + if (userCompletedByLeast !== userCompletedByMost) { + historyInfo.push({ + icon: ( + <Avatar> + { + performers.find(p => p.userId === userCompletedByLeast) + ?.displayName + } + </Avatar> + ), + text: `${ + performers.find(p => p.userId === Number(userCompletedByLeast)) + .displayName + } completed least`, + subtext: `${userHistories[userCompletedByLeast]} time/s`, + }) + } + + setHistoryInfo(historyInfo) + } + + function formatTimeDifference(startDate, endDate) { + const diffInMinutes = moment(startDate).diff(endDate, 'minutes') + let timeValue = diffInMinutes + let unit = 'minute' + + if (diffInMinutes >= 60) { + const diffInHours = moment(startDate).diff(endDate, 'hours') + timeValue = diffInHours + unit = 'hour' + + if (diffInHours >= 24) { + const diffInDays = moment(startDate).diff(endDate, 'days') + timeValue = diffInDays + unit = 'day' + } + } + + return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}` + } + if (isLoading) { + return <CircularProgress /> // Show loading indicator + } + if (!choreHistory.length) { + return ( + <Container + maxWidth='md' + sx={{ + textAlign: 'center', + display: 'flex', + // make sure the content is centered vertically: + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + height: '50vh', + }} + > + <EventBusy + sx={{ + fontSize: '6rem', + // color: 'text.disabled', + mb: 1, + }} + /> + + <Typography level='h3' gutterBottom> + No History Yet + </Typography> + <Typography level='body1'> + You haven't completed any tasks. Once you start finishing tasks, + they'll show up here. + </Typography> + <Button variant='soft' sx={{ mt: 2 }}> + <Link to='/my/chores'>Go back to chores</Link> + </Button> + </Container> + ) + } + + return ( + <Container maxWidth='md'> + <Typography level='h3' mb={1.5}> + Summary: + </Typography> + {/* <Sheet sx={{ mb: 1, borderRadius: 'sm', p: 2, boxShadow: 'md' }}> + <ListItem sx={{ gap: 1.5 }}> + <ListItemDecorator> + <Avatar> + <AccountCircle /> + </Avatar> + </ListItemDecorator> + <ListItemContent> + <Typography level='body1' sx={{ fontWeight: 'md' }}> + {choreHistory.length} completed + </Typography> + <Typography level='body2' color='text.tertiary'> + {Object.keys(userHistory).length} users contributed + </Typography> + </ListItemContent> + </ListItem> + </Sheet> */} + <Grid container> + {historyInfo.map((info, index) => ( + <Grid key={index} item xs={12} sm={6}> + <Sheet sx={{ mb: 1, borderRadius: 'sm', p: 2, boxShadow: 'md' }}> + <ListItem sx={{ gap: 1.5 }}> + <ListItemDecorator>{info.icon}</ListItemDecorator> + <ListItemContent> + <Typography level='body1' sx={{ fontWeight: 'md' }}> + {info.text} + </Typography> + <Typography level='body1' color='text.tertiary'> + {info.subtext} + </Typography> + </ListItemContent> + </ListItem> + </Sheet> + </Grid> + ))} + </Grid> + {/* User History Cards */} + <Typography level='h3' my={1.5}> + History: + </Typography> + <Box sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}> + {/* Chore History List (Updated Style) */} + + <List sx={{ p: 0 }}> + {choreHistory.map((chore, index) => ( + <> + <ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}> + {' '} + {/* Adjusted spacing and alignment */} + <ListItemDecorator> + <Avatar sx={{ mr: 1 }}> + {performers + .find(p => p.userId === chore.completedBy) + ?.displayName?.charAt(0) || '?'} + </Avatar> + </ListItemDecorator> + <ListItemContent sx={{ my: 0 }}> + {' '} + {/* Removed vertical margin */} + <Box + sx={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }} + > + <Typography level='body1' sx={{ fontWeight: 'md' }}> + {moment(chore.completedAt).format('ddd MM/DD/yyyy HH:mm')} + </Typography> + + <Chip> + {chore.dueDate && chore.completedAt > chore.dueDate + ? 'Late' + : 'On Time'} + </Chip> + </Box> + <Typography level='body2' color='text.tertiary'> + <Chip> + { + performers.find(p => p.userId === chore.completedBy) + ?.displayName + } + </Chip>{' '} + completed + {chore.completedBy !== chore.assignedTo && ( + <> + {', '} + assigned to{' '} + <Chip> + { + performers.find(p => p.userId === chore.assignedTo) + ?.displayName + } + </Chip> + </> + )} + </Typography> + {chore.dueDate && ( + <Typography level='body2' color='text.tertiary'> + Due: {moment(chore.dueDate).format('ddd MM/DD/yyyy')} + </Typography> + )} + {chore.notes && ( + <Typography level='body2' color='text.tertiary'> + Note: {chore.notes} + </Typography> + )} + </ListItemContent> + </ListItem> + {index < choreHistory.length - 1 && ( + <> + <ListDivider component='li'> + {/* time between two completion: */} + {index < choreHistory.length - 1 && + choreHistory[index + 1].completedAt && ( + <Typography level='body3' color='text.tertiary'> + {formatTimeDifference( + chore.completedAt, + choreHistory[index + 1].completedAt, + )}{' '} + before + </Typography> + )} + </ListDivider> + </> + )} + </> + ))} + </List> + </Box> + </Container> + ) +} + +export default ChoreHistory diff --git a/src/views/History/InfoCard.jsx b/src/views/History/InfoCard.jsx new file mode 100644 index 0000000..bf1ada8 --- /dev/null +++ b/src/views/History/InfoCard.jsx @@ -0,0 +1,26 @@ +import { AddTask } from '@mui/icons-material' +import { Box } from '@mui/joy' +import Card from '@mui/joy/Card' +import CardContent from '@mui/joy/CardContent' +import Typography from '@mui/joy/Typography' +import * as React from 'react' + +function InfoCard() { + return ( + <Card sx={{ minWidth: 200, maxWidth: 200 }}> + <CardContent> + <Box mb={2} sx={{ textAlign: 'left' }}> + <AddTask + sx={{ + fontSize: '2.5em' /* Increase the font size */, + }} + /> + </Box> + <Typography level='title-md'>You've completed</Typography> + <Typography level='body-sm'>12345 Chores</Typography> + </CardContent> + </Card> + ) +} + +export default InfoCard diff --git a/src/views/Home.jsx b/src/views/Home.jsx new file mode 100644 index 0000000..4b16a1e --- /dev/null +++ b/src/views/Home.jsx @@ -0,0 +1,46 @@ +import { Box, Button, Container, Typography } from '@mui/joy' +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' + +import { useState } from 'react' +import Logo from '../Logo' +const Home = () => { + const Navigate = useNavigate() + const getCurrentUser = () => { + return JSON.parse(localStorage.getItem('user')) + } + const [users, setUsers] = useState([]) + const [currentUser, setCurrentUser] = useState(getCurrentUser()) + + useEffect(() => {}, []) + + return ( + <Container className='flex h-full items-center justify-center'> + <Box className='flex flex-col items-center justify-center'> + <Logo /> + <Typography level='h1'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + </Box> + <Box className='flex flex-col items-center justify-center' mt={10}> + <Button + sx={{ mt: 1 }} + onClick={() => { + Navigate('/my/chores') + }} + > + Get Started! + </Button> + </Box> + </Container> + ) +} + +export default Home diff --git a/src/views/Landing/FeaturesSection.jsx b/src/views/Landing/FeaturesSection.jsx new file mode 100644 index 0000000..a7da1f0 --- /dev/null +++ b/src/views/Landing/FeaturesSection.jsx @@ -0,0 +1,139 @@ +import { + AutoAwesomeMosaicOutlined, + AutoAwesomeRounded, + CodeRounded, + GroupRounded, + HistoryRounded, + Webhook, +} from '@mui/icons-material' +import Card from '@mui/joy/Card' +import Container from '@mui/joy/Container' +import Typography from '@mui/joy/Typography' +import { styled } from '@mui/system' + +const FeatureIcon = styled('div')({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#f0f0f0', // Adjust the background color as needed + borderRadius: '50%', + minWidth: '60px', + height: '60px', + marginRight: '16px', +}) + +const CardData = [ + { + title: 'Open Source & Transparent', + headline: 'Built for the Community', + description: + 'Donetick is a community-driven, open-source project. Contribute, customize, and make task management truly yours.', + icon: CodeRounded, + }, + { + title: 'Circles: Your Task Hub', + headline: 'Share & Conquer Together', + description: + 'Create circles for your family, friends, or team. Easily share tasks and track progress within each group.', + icon: GroupRounded, + }, + { + title: 'Track Your Progress', + headline: "See Who's Done What", + description: + 'View a history of task completion for each member of your circles. Celebrate successes and stay on top of your goals.', + icon: HistoryRounded, + }, + { + title: 'Automated Chore Scheduling', + headline: 'Fully Customizable Recurring Tasks', + description: + 'Set up chores to repeat daily, weekly, or monthly. Donetick will automatically assign and track each task for you.', + icon: AutoAwesomeMosaicOutlined, + }, + { + title: 'Automated Task Assignment', + headline: 'Share Responsibilities Equally', + description: + 'can automatically assigns tasks to each member of your circle. Randomly or based on past completion.', + icon: AutoAwesomeRounded, + }, + { + title: 'Integrations & Webhooks', + headline: 'API & 3rd Party Integrations', + description: + 'Connect Donetick with your favorite apps and services. Trigger tasks based on events from other platforms.', + icon: Webhook, + }, +] + +function Feature2({ icon: Icon, title, headline, description, index }) { + return ( + <Card + variant='plain' + sx={{ textAlign: 'left', p: 2 }} + data-aos-delay={100 * index} + data-aos-anchor='[data-aos-id-features2-blocks]' + data-aos='fade-up' + > + <div style={{ display: 'flex', alignItems: 'center' }}> + <FeatureIcon> + <Icon + color='primary' + style={{ Width: '30px', height: '30px' }} + stroke={1.5} + /> + </FeatureIcon> + <div> + {/* Changes are within this div */} + <Typography level='h4' mt={1} mb={0.5}> + {title} + </Typography> + <Typography level='body-sm' color='neutral' lineHeight={1.4}> + {headline} + </Typography> + </div> + </div> + <Typography level='body-md' color='neutral' lineHeight={1.6}> + {description} + </Typography> + </Card> + ) +} + +function FeaturesSection() { + const features = CardData.map((feature, index) => ( + <Feature2 + icon={feature.icon} + title={feature.title} + headline={feature.headline} + description={feature.description} + index={index} + key={index} + /> + )) + + return ( + <Container sx={{ textAlign: 'center' }}> + <Typography level='h4' mt={2} mb={4}> + Donetick + </Typography> + + <Container maxWidth={'lg'} sx={{ mb: 8 }}> + <Typography level='body-md' color='neutral'> + Navigate personal growth with genuine insights, thoughtful privacy, + and actionable steps tailored just for you. + </Typography> + </Container> + + <div + className='align-center mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3' + data-aos-id-features2-blocks + > + {features} + </div> + </Container> + ) +} + +export default FeaturesSection diff --git a/src/views/Landing/HomeHero.jsx b/src/views/Landing/HomeHero.jsx new file mode 100644 index 0000000..a96374a --- /dev/null +++ b/src/views/Landing/HomeHero.jsx @@ -0,0 +1,186 @@ +/* eslint-disable tailwindcss/no-custom-classname */ +// import { StyledButton } from '@/components/styled-button' +import { Button } from '@mui/joy' +import Typography from '@mui/joy/Typography' +import Box from '@mui/material/Box' +import Grid from '@mui/material/Grid' +import React from 'react' +import { useNavigate } from 'react-router-dom' + +import Logo from '@/assets/logo.svg' +import screenShotMyChore from '@/assets/screenshot-my-chore.png' +import { GitHub } from '@mui/icons-material' + +const HomeHero = () => { + const navigate = useNavigate() + const HERO_TEXT_THAT = [ + // 'Donetick simplifies the entire process, from scheduling and reminders to automatic task assignment and progress tracking.', + // 'Donetick is the intuitive task and chore management app designed for groups. Take charge of shared responsibilities, automate your workflow, and achieve more together.', + 'An open-source, user-friendly app for managing tasks and chores, featuring customizable options to help you and others stay organized', + ] + + const [heroTextIndex, setHeroTextIndex] = React.useState(0) + + React.useEffect(() => { + // const intervalId = setInterval( + // () => setHeroTextIndex(index => index + 1), + // 4000, // every 4 seconds + // ) + // return () => clearTimeout(intervalId) + }, []) + + const Title = () => ( + <Box + sx={{ + textAlign: 'center', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }} + > + <img src={Logo} width={'100px'} /> + <Typography level='h1' fontSize={58} fontWeight={800}> + <span + data-aos-delay={50 * 1} + data-aos-anchor='[data-aos-id-hero]' + data-aos='fade-up' + > + Done + </span> + <span + data-aos-delay={100 * 3} + data-aos-anchor='[data-aos-id-hero]' + data-aos='fade-up' + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + </Box> + ) + + const Subtitle = () => ( + <Typography + level='h2' + fontWeight={500} + textAlign={'center'} + className='opacity-70' + data-aos-delay={100 * 5} + data-aos-anchor='[data-aos-id-hero]' + data-aos='zoom-in' + > + Simplify Tasks & Chores, Together. + </Typography> + ) + + const CTAButton = () => ( + <Button + data-aos-delay={100 * 2} + data-aos-anchor='[data-aos-id-hero]' + data-aos='fade-up' + variant='solid' + size='lg' + sx={{ + py: 1.25, + px: 5, + fontSize: 20, + mt: 2, + borderWidth: 3, + // boxShadow: '0px 0px 24px rgba(81, 230, 221, 0.5)', + transition: 'all 0.20s', + }} + className='hover:scale-105' + onClick={() => { + // if the url is donetick.com then navigate to app.donetick.com/my/chores + // else navigate to /my/chores + if (window.location.hostname === 'donetick.com') { + window.location.href = 'https://app.donetick.com/my/chores' + } else { + navigate('/my/chores') + } + }} + > + Get started + </Button> + ) + + return ( + // <Box + // id='hero' + // className='grid min-h-[90vh] w-full place-items-center px-4 py-12' + // data-aos-id-hero + // > + <Grid container spacing={16} sx={{ py: 12 }}> + <Grid item xs={12} md={7}> + <Title /> + <div className='flex flex-col gap-6'> + <Subtitle /> + + <Typography + level='title-lg' + textAlign={'center'} + fontSize={28} + // textColor={'#06b6d4'} + color='primary' + data-aos-delay={100 * 1} + data-aos-anchor='[data-aos-id-hero]' + data-aos='fade-up' + > + {`"${HERO_TEXT_THAT[heroTextIndex % HERO_TEXT_THAT.length]}"`} + </Typography> + + <Box className='flex w-full justify-center'> + <CTAButton /> + <Button + data-aos-delay={100 * 2.5} + data-aos-anchor='[data-aos-id-hero]' + data-aos='fade-up' + variant='soft' + size='lg' + sx={{ + py: 1.25, + px: 5, + ml: 2, + fontSize: 20, + mt: 2, + borderWidth: 3, + // boxShadow: '0px 0px 24px rgba(81, 230, 221, 0.5)', + transition: 'all 0.20s', + }} + className='hover:scale-105' + onClick={() => { + // new window open to https://github.com/Donetick: + window.open('https://github.com/donetick', '_blank') + }} + startDecorator={<GitHub />} + > + Github + </Button> + </Box> + </div> + </Grid> + + <Grid item xs={12} md={5}> + <div className='flex justify-center'> + <img + src={screenShotMyChore} + width={'100%'} + style={{ + maxWidth: 300, + }} + height={'auto'} + alt='Hero img' + data-aos-delay={100 * 2} + data-aos-anchor='[data-aos-id-hero]' + data-aos='fade-left' + /> + </div> + </Grid> + </Grid> + ) +} + +export default HomeHero diff --git a/src/views/Landing/Landing.jsx b/src/views/Landing/Landing.jsx new file mode 100644 index 0000000..2041e42 --- /dev/null +++ b/src/views/Landing/Landing.jsx @@ -0,0 +1,32 @@ +import { Container } from '@mui/joy' +import AOS from 'aos' +import 'aos/dist/aos.css' +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import FeaturesSection from './FeaturesSection' +import HomeHero from './HomeHero' +import PricingSection from './PricingSection' +const Landing = () => { + const Navigate = useNavigate() + const getCurrentUser = () => { + return JSON.parse(localStorage.getItem('user')) + } + const [users, setUsers] = useState([]) + const [currentUser, setCurrentUser] = useState(getCurrentUser()) + + useEffect(() => { + AOS.init({ + once: false, // whether animation should happen only once - while scrolling down + }) + }, []) + + return ( + <Container className='flex h-full items-center justify-center'> + <HomeHero /> + <FeaturesSection /> + <PricingSection /> + </Container> + ) +} + +export default Landing diff --git a/src/views/Landing/PricingSection.jsx b/src/views/Landing/PricingSection.jsx new file mode 100644 index 0000000..634cf7d --- /dev/null +++ b/src/views/Landing/PricingSection.jsx @@ -0,0 +1,179 @@ +/* eslint-disable react/jsx-key */ +import { CheckRounded } from '@mui/icons-material' +import { Box, Button, Card, Container, Typography } from '@mui/joy' +import React from 'react' +import { useNavigate } from 'react-router-dom' + +const PricingSection = () => { + const navigate = useNavigate() + const FEATURES_FREE = [ + ['Create Tasks and Chores', <CheckRounded color='primary' />], + ['Limited Task History', <CheckRounded color='primary' />], + ['Circle up to two members', <CheckRounded color='primary' />], + ] + const FEATURES_PREMIUM = [ + ['All Basic Features', <CheckRounded color='primary' />], + ['Hosted on DoneTick servers', <CheckRounded color='primary' />], + ['Up to 8 Circle Members', <CheckRounded color='primary' />], + [ + 'Notification through Telegram (Discord coming soon)', + <CheckRounded color='primary' />, + ], + ['Unlimited History', <CheckRounded color='primary' />], + [ + 'All circle members get the same features as the owner', + <CheckRounded color='primary' />, + ], + ] + const FEATURES_YEARLY = [ + // ['All Basic Features', <CheckRounded color='primary' />], + // ['Up to 8 Circle Members', <CheckRounded color='primary' />], + ['Notification through Telegram bot', <CheckRounded color='primary' />], + ['Custom Webhook/API Integration', <CheckRounded color='primary' />], + ['Unlimited History', <CheckRounded color='primary' />], + + ['Priority Support', <CheckRounded color='primary' />], + ] + const PRICEITEMS = [ + { + title: 'Basic', + description: + 'Hosted on Donetick servers, supports up to 2 circle members and includes all the features of the free plan.', + price: 0, + previousPrice: 0, + interval: 'month', + discount: false, + features: FEATURES_FREE, + }, + + { + title: 'Plus', + description: + // 'Supports up to 8 circle members and includes all the features of the Basic plan.', + 'Hosted on Donetick servers, supports up to 8 circle members and includes all the features of the Basic plan.', + price: 30.0, + // previousPrice: 76.89, + interval: 'year', + // discount: true, + features: FEATURES_YEARLY, + }, + ] + return ( + <Container + sx={{ textAlign: 'center', mb: 2 }} + maxWidth={'lg'} + id='pricing-tiers' + > + <Typography level='h4' mt={2} mb={2}> + Pricing + </Typography> + + <Container maxWidth={'sm'} sx={{ mb: 8 }}> + <Typography level='body-md' color='neutral'> + Choose the plan that works best for you. + </Typography> + </Container> + + <div + className='mt-8 grid grid-cols-1 gap-2 sm:grid-cols-1 lg:grid-cols-2' + data-aos-id-pricing + > + {PRICEITEMS.map((pi, index) => ( + <Card + key={index} + data-aos-delay={50 * (1 + index)} + data-aos-anchor='[data-aos-id-pricing]' + data-aos='fade-up' + className='hover:bg-white dark:hover:bg-teal-900' + sx={{ + textAlign: 'center', + p: 5, + minHeight: 400, + // maxWidth: 400, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + // when top reach the top change the background color: + '&:hover': { + // backgroundColor: '#FFFFFF', + boxShadow: '0px 0px 20px rgba(0, 0, 0, 0.1)', + }, + }} + > + <Box + display='flex' + flexDirection='column' + justifyContent='flex-start' // Updated property + alignItems='center' + > + <Typography level='h2'>{pi.title}</Typography> + <Typography level='body-md'>{pi.description}</Typography> + </Box> + <Box + display='flex' + flexDirection='column' + justifyContent='center' + alignItems='center' + > + <Box + display='flex' + flexDirection='row' + alignItems='baseline' + sx={{ my: 4 }} + > + {pi.discount && ( + <Typography + level='h3' + component='span' + sx={{ textDecoration: 'line-through', opacity: 0.5 }} + > + ${pi.previousPrice} + </Typography> + )} + <Typography level='h2' component='span'> + ${pi.price} + </Typography> + <Typography level='body-md' component='span'> + / {pi.interval} + </Typography> + </Box> + + <Typography level='title-md'>Features</Typography> + {pi.features.map(feature => ( + <Typography + startDecorator={feature[1]} + level='body-md' + color='neutral' + lineHeight={1.6} + > + {feature[0]} + </Typography> + ))} + + {/* Here start the test */} + <div style={{ marginTop: 'auto' }}> + <Button + sx={{ mt: 5 }} + onClick={() => { + navigate('/settings#account') + }} + > + Get Started + </Button> + <Typography + level='body-md' + color='neutral' + lineHeight={1.6} + ></Typography> + </div> + </Box> + </Card> + ))} + </div> + + {/* Here start the test */} + </Container> + ) +} + +export default PricingSection diff --git a/src/views/Modals/Inputs/ConfirmationModal.jsx b/src/views/Modals/Inputs/ConfirmationModal.jsx new file mode 100644 index 0000000..10f9bee --- /dev/null +++ b/src/views/Modals/Inputs/ConfirmationModal.jsx @@ -0,0 +1,43 @@ +import { Box, Button, Modal, ModalDialog, Typography } from '@mui/joy' +import React from 'react' + +function ConfirmationModal({ config }) { + const handleAction = isConfirmed => { + config.onClose(isConfirmed) + } + + return ( + <Modal open={config?.isOpen} onClose={config?.onClose}> + <ModalDialog> + <Typography level='h4' mb={1}> + {config?.title} + </Typography> + + <Typography level='body-md' gutterBottom> + {config?.message} + </Typography> + + <Box display={'flex'} justifyContent={'space-around'} mt={1}> + <Button + onClick={() => { + handleAction(true) + }} + fullWidth + sx={{ mr: 1 }} + > + {config?.confirmText} + </Button> + <Button + onClick={() => { + handleAction(false) + }} + variant='outlined' + > + {config?.cancelText} + </Button> + </Box> + </ModalDialog> + </Modal> + ) +} +export default ConfirmationModal diff --git a/src/views/Modals/Inputs/CreateThingModal.jsx b/src/views/Modals/Inputs/CreateThingModal.jsx new file mode 100644 index 0000000..59263ff --- /dev/null +++ b/src/views/Modals/Inputs/CreateThingModal.jsx @@ -0,0 +1,112 @@ +import { + Box, + Button, + FormLabel, + Input, + Modal, + ModalDialog, + Option, + Select, + Textarea, + Typography, +} from '@mui/joy' +import { useEffect, useState } from 'react' + +function CreateThingModal({ isOpen, onClose, onSave, currentThing }) { + const [name, setName] = useState(currentThing?.name || '') + const [type, setType] = useState(currentThing?.type || 'numeric') + const [state, setState] = useState(currentThing?.state || '') + useEffect(() => { + if (type === 'boolean') { + if (state !== 'true' && state !== 'false') { + setState('false') + } + } else if (type === 'number') { + if (isNaN(state)) { + setState(0) + } + } + }, [type]) + const handleSave = () => { + onSave({ name, type, id: currentThing?.id, state: state || null }) + onClose() + } + + return ( + <Modal open={isOpen} onClose={onClose}> + <ModalDialog> + {/* <ModalClose /> */} + <Typography variant='h4'>P;lease add info</Typography> + <FormLabel>Name</FormLabel> + + <Textarea + placeholder='Thing name' + value={name} + onChange={e => setName(e.target.value)} + sx={{ minWidth: 300 }} + /> + <FormLabel>Type</FormLabel> + <Select value={type} sx={{ minWidth: 300 }}> + {['text', 'number', 'boolean'].map(type => ( + <Option value={type} key={type} onClick={() => setType(type)}> + {type.charAt(0).toUpperCase() + type.slice(1)} + </Option> + ))} + </Select> + + {type === 'text' && ( + <> + <FormLabel>Value</FormLabel> + <Input + placeholder='Thing value' + value={state || ''} + onChange={e => setState(e.target.value)} + sx={{ minWidth: 300 }} + /> + </> + )} + {type === 'number' && ( + <> + <FormLabel>Value</FormLabel> + <Input + placeholder='Thing value' + type='number' + value={state || ''} + onChange={e => { + setState(e.target.value) + }} + sx={{ minWidth: 300 }} + /> + </> + )} + {type === 'boolean' && ( + <> + <FormLabel>Value</FormLabel> + <Select sx={{ minWidth: 300 }} value={state}> + {['true', 'false'].map(value => ( + <Option + value={value} + key={value} + onClick={() => setState(value)} + > + {value.charAt(0).toUpperCase() + value.slice(1)} + </Option> + ))} + </Select> + </> + )} + + <Box display={'flex'} justifyContent={'space-around'} mt={1}> + <Button onClick={handleSave} fullWidth sx={{ mr: 1 }}> + {currentThing?.id ? 'Update' : 'Create'} + </Button> + <Button onClick={onClose} variant='outlined'> + {currentThing?.id ? 'Cancel' : 'Close'} + </Button> + </Box> + </ModalDialog> + </Modal> + ) +} + +export default CreateThingModal diff --git a/src/views/Modals/Inputs/DateModal.jsx b/src/views/Modals/Inputs/DateModal.jsx new file mode 100644 index 0000000..34319c3 --- /dev/null +++ b/src/views/Modals/Inputs/DateModal.jsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react' +import { + Modal, + Button, + Input, + ModalDialog, + ModalClose, + Box, + Typography, +} from '@mui/joy' + +function DateModal({ isOpen, onClose, onSave, current, title }) { + const [date, setDate] = useState( + current ? new Date(current).toISOString().split('T')[0] : null, + ) + + const handleSave = () => { + onSave(date) + onClose() + } + + return ( + <Modal open={isOpen} onClose={onClose}> + <ModalDialog> + {/* <ModalClose /> */} + <Typography variant='h4'>{title}</Typography> + <Input + sx={{ mt: 3 }} + type='date' + value={date} + onChange={e => setDate(e.target.value)} + /> + <Box display={'flex'} justifyContent={'space-around'} mt={1}> + <Button onClick={handleSave} fullWidth sx={{ mr: 1 }}> + Save + </Button> + <Button onClick={onClose} variant='outlined'> + Cancel + </Button> + </Box> + </ModalDialog> + </Modal> + ) +} +export default DateModal diff --git a/src/views/Modals/Inputs/SelectModal.jsx b/src/views/Modals/Inputs/SelectModal.jsx new file mode 100644 index 0000000..61e7ae9 --- /dev/null +++ b/src/views/Modals/Inputs/SelectModal.jsx @@ -0,0 +1,49 @@ +import { + Box, + Button, + Modal, + ModalDialog, + Option, + Select, + Typography, +} from '@mui/joy' +import React from 'react' + +function SelectModal({ isOpen, onClose, onSave, options, title, displayKey }) { + const [selected, setSelected] = React.useState(null) + const handleSave = () => { + onSave(options.find(item => item.id === selected)) + onClose() + } + + return ( + <Modal open={isOpen} onClose={onClose}> + <ModalDialog> + <Typography variant='h4'>{title}</Typography> + <Select> + {options.map((item, index) => ( + <Option + value={item.id} + key={item[displayKey]} + onClick={() => { + setSelected(item.id) + }} + > + {item[displayKey]} + </Option> + ))} + </Select> + + <Box display={'flex'} justifyContent={'space-around'} mt={1}> + <Button onClick={handleSave} fullWidth sx={{ mr: 1 }}> + Save + </Button> + <Button onClick={onClose} variant='outlined'> + Cancel + </Button> + </Box> + </ModalDialog> + </Modal> + ) +} +export default SelectModal diff --git a/src/views/Modals/Inputs/TextModal.jsx b/src/views/Modals/Inputs/TextModal.jsx new file mode 100644 index 0000000..2b44f78 --- /dev/null +++ b/src/views/Modals/Inputs/TextModal.jsx @@ -0,0 +1,46 @@ +import { Box, Button, Modal, ModalDialog, Textarea, Typography } from '@mui/joy' +import { useState } from 'react' + +function TextModal({ + isOpen, + onClose, + onSave, + current, + title, + okText, + cancelText, +}) { + const [text, setText] = useState(current) + + const handleSave = () => { + onSave(text) + onClose() + } + + return ( + <Modal open={isOpen} onClose={onClose}> + <ModalDialog> + {/* <ModalClose /> */} + <Typography variant='h4'>{title}</Typography> + <Textarea + placeholder='Type in here…' + value={text} + onChange={e => setText(e.target.value)} + minRows={2} + maxRows={4} + sx={{ minWidth: 300 }} + /> + + <Box display={'flex'} justifyContent={'space-around'} mt={1}> + <Button onClick={handleSave} fullWidth sx={{ mr: 1 }}> + {okText ? okText : 'Save'} + </Button> + <Button onClick={onClose} variant='outlined'> + {cancelText ? cancelText : 'Cancel'} + </Button> + </Box> + </ModalDialog> + </Modal> + ) +} +export default TextModal diff --git a/src/views/NotificationTargets/EditNotificationTarget.jsx b/src/views/NotificationTargets/EditNotificationTarget.jsx new file mode 100644 index 0000000..ba1a38a --- /dev/null +++ b/src/views/NotificationTargets/EditNotificationTarget.jsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' + +const EditNotificationTarget = () => { + const { id } = useParams() + const [notificationTarget, setNotificationTarget] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + // const fetchNotificationTarget = async () => { + // try { + // const response = await fetch(`/api/notification-targets/${id}`) + // const data = await response.json() + // setNotificationTarget(data) + // } catch (error) { + // setError(error) + // } finally { + // setLoading(false) + // } + // } + // fetchNotificationTarget() + }, [id]) + + if (loading) { + return <div>Loading...</div> + } + + if (error) { + return <div>Error: {error.message}</div> + } + + return ( + <div> + <h1>Edit Notification Target</h1> + <form> + <label> + Name: + <input type='text' value={notificationTarget.name} /> + </label> + <label> + Email: + <input type='email' value={notificationTarget.email} /> + </label> + <button type='submit'>Save</button> + </form> + </div> + ) +} + +export default EditNotificationTarget diff --git a/src/views/Payments/PaymentFailView.jsx b/src/views/Payments/PaymentFailView.jsx new file mode 100644 index 0000000..38fcc2a --- /dev/null +++ b/src/views/Payments/PaymentFailView.jsx @@ -0,0 +1,51 @@ +import { Box, Container, Sheet, Typography } from '@mui/joy' +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import Logo from '../../Logo' + +const PaymentCancelledView = () => { + const navigate = useNavigate() + + useEffect(() => { + const timer = setTimeout(() => { + navigate('/my/chores') + }, 5000) + return () => clearTimeout(timer) + }, [navigate]) + + return ( + <Container component='main' maxWidth='xs'> + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Sheet + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + <Logo /> + <Typography level='h2' sx={{ mt: 2, mb: 1 }}> + Payment has been cancelled + </Typography> + <Typography level='body-md' sx={{ mb: 2 }}> + You will be redirected to the main page shortly. + </Typography> + </Sheet> + </Box> + </Container> + ) +} + +export default PaymentCancelledView diff --git a/src/views/Payments/PaymentSuccessView.jsx b/src/views/Payments/PaymentSuccessView.jsx new file mode 100644 index 0000000..b2fbb50 --- /dev/null +++ b/src/views/Payments/PaymentSuccessView.jsx @@ -0,0 +1,51 @@ +import { Box, Container, Sheet, Typography } from '@mui/joy' +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import Logo from '../../Logo' + +const PaymentSuccessView = () => { + const navigate = useNavigate() + + useEffect(() => { + const timer = setTimeout(() => { + navigate('/settings') + }, 5000) + return () => clearTimeout(timer) + }, [navigate]) + + return ( + <Container component='main' maxWidth='xs'> + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Sheet + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + <Logo /> + <Typography level='h2' sx={{ mt: 2, mb: 1 }}> + Payment Successful! + </Typography> + <Typography level='body-md' sx={{ mb: 2 }}> + You will be redirected to the settings page shortly. + </Typography> + </Sheet> + </Box> + </Container> + ) +} + +export default PaymentSuccessView diff --git a/src/views/PrivacyPolicy/PrivacyPolicyView.jsx b/src/views/PrivacyPolicy/PrivacyPolicyView.jsx new file mode 100644 index 0000000..42a4707 --- /dev/null +++ b/src/views/PrivacyPolicy/PrivacyPolicyView.jsx @@ -0,0 +1,102 @@ +import React from 'react' + +const PrivacyPolicyView = () => { + return ( + <div> + <h1>Privacy Policy</h1> + <p> + Favoro LLC ("we," "us," or "our") operates the Donetick application and + website (collectively, the "Service"). This Privacy Policy informs you + of our policies regarding the collection, use, and disclosure of + personal data when you use our Service and the choices you have + associated with that data. + </p> + <h2>Information We Collect</h2> + <p> + <strong>Personal Data:</strong> When you register for an account or use + the Service, we may collect certain personally identifiable information, + such as your name and email address. + </p> + <p> + <strong>Usage Data:</strong> We collect information on how you use the + Service, such as your IP address, browser type, pages visited, and the + time and date of your visit. + </p> + <p> + <strong>Task Data:</strong> We store the tasks and chores you create + within the app, including their details and any assigned users. + </p> + <h2>How We Use Your Information</h2> + <p> + <strong>Provide and Maintain the Service:</strong> We use your + information to operate, maintain, and improve the Service. + </p> + <p> + <strong>Communicate with You:</strong> We may use your email address to + send you notifications, updates, and promotional materials related to + the Service. + </p> + <p> + <strong>Analyze Usage:</strong> We analyze usage data to understand how + the Service is used and to make improvements. + </p> + <h2>How We Share Your Information</h2> + <p> + <strong>With Your Consent:</strong> We will not share your personal data + with third parties without your consent, except as described in this + Privacy Policy. + </p> + <p> + <strong>Service Providers:</strong> We may engage third-party companies + or individuals to perform services on our behalf (e.g., hosting, + analytics). These third parties have access to your personal data only + to perform these tasks and are obligated not to disclose or use it for + any other purpose. + </p> + <p> + <strong>Compliance with Law:</strong> We may disclose your personal data + if required to do so by law or in response to valid requests by public + authorities (e.g., a court or government agency). + </p> + <h2>Security</h2> + <p> + We value your privacy and have implemented reasonable security measures + to protect your personal data from unauthorized access, disclosure, + alteration, or destruction. However, no method of transmission over the + Internet or electronic storage is 100% secure, and we cannot guarantee + absolute security. + </p> + <h2>Your Choices</h2> + <p> + <strong>Account Information:</strong> You can update or correct your + account information at any time. + </p> + <p> + <strong>Marketing Communications:</strong> You can opt out of receiving + promotional emails by following the unsubscribe instructions included in + those emails. + </p> + <h2>Children's Privacy</h2> + <p> + Our Service is not intended for children under 13 years of age. We do + not knowingly collect personal data from children under 13. If you are a + parent or guardian and you are aware that your child has provided us + with personal data, please contact us. + </p> + <h2>Changes to This Privacy Policy</h2> + <p> + We may update our Privacy Policy from time to time. We will notify you + of any changes by posting the new Privacy Policy on this page and + updating the "Effective Date" at the top of this Privacy Policy. + </p> + <h2>Contact Us</h2> + <p> + If you have any questions about this Privacy Policy, please contact us + at: + </p> + <p>Favoro LLC</p> + </div> + ) +} + +export default PrivacyPolicyView diff --git a/src/views/Settings/APITokenSettings.jsx b/src/views/Settings/APITokenSettings.jsx new file mode 100644 index 0000000..5bd9887 --- /dev/null +++ b/src/views/Settings/APITokenSettings.jsx @@ -0,0 +1,130 @@ +import { Box, Button, Card, Chip, Divider, Typography } from '@mui/joy' +import moment from 'moment' +import { useContext, useEffect, useState } from 'react' +import { UserContext } from '../../contexts/UserContext' +import { + CreateLongLiveToken, + DeleteLongLiveToken, + GetLongLiveTokens, +} from '../../utils/Fetcher' +import { isPlusAccount } from '../../utils/Helpers' +import TextModal from '../Modals/Inputs/TextModal' + +const APITokenSettings = () => { + const [tokens, setTokens] = useState([]) + const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false) + const { userProfile, setUserProfile } = useContext(UserContext) + useEffect(() => { + GetLongLiveTokens().then(resp => { + resp.json().then(data => { + setTokens(data.res) + }) + }) + }, []) + + const handleSaveToken = name => { + CreateLongLiveToken(name).then(resp => { + if (resp.ok) { + resp.json().then(data => { + // add the token to the list: + console.log(data) + const newTokens = [...tokens] + newTokens.push(data.res) + setTokens(newTokens) + }) + } + }) + } + + return ( + <div className='grid gap-4 py-4' id='apitokens'> + <Typography level='h3'>Long Live Token</Typography> + <Divider /> + <Typography level='body-sm'> + Create token to use with the API to update things that trigger task or + chores + </Typography> + {!isPlusAccount(userProfile) && ( + <Chip variant='soft' color='warning'> + Not available in Basic Plan + </Chip> + )} + + {tokens.map(token => ( + <Card key={token.token} className='p-4'> + <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> + <Box> + <Typography level='body-md'>{token.name}</Typography> + <Typography level='body-xs'> + {moment(token.createdAt).fromNow()}( + {moment(token.createdAt).format('lll')}) + </Typography> + </Box> + <Box> + {token.token && ( + <Button + variant='outlined' + color='primary' + sx={{ mr: 1 }} + onClick={() => { + navigator.clipboard.writeText(token.token) + alert('Token copied to clipboard') + }} + > + Copy Token + </Button> + )} + + <Button + variant='outlined' + color='danger' + onClick={() => { + const confirmed = confirm( + `Are you sure you want to remove ${token.name} ?`, + ) + if (confirmed) { + DeleteLongLiveToken(token.id).then(resp => { + if (resp.ok) { + alert('Token removed') + const newTokens = tokens.filter(t => t.id !== token.id) + setTokens(newTokens) + } + }) + } + }} + > + Remove + </Button> + </Box> + </Box> + </Card> + ))} + + <Button + variant='soft' + color='primary' + disabled={!isPlusAccount(userProfile)} + sx={{ + width: '210px', + mb: 1, + }} + onClick={() => { + setIsGetTokenNameModalOpen(true) + }} + > + Generate New Token + </Button> + <TextModal + isOpen={isGetTokenNameModalOpen} + title='Give a name for your new token, something to remember it by.' + onClose={() => { + setIsGetTokenNameModalOpen(false) + }} + okText={'Generate Token'} + onSave={handleSaveToken} + /> + </div> + ) +} + +export default APITokenSettings diff --git a/src/views/Settings/NotificationSetting.jsx b/src/views/Settings/NotificationSetting.jsx new file mode 100644 index 0000000..4ead3b9 --- /dev/null +++ b/src/views/Settings/NotificationSetting.jsx @@ -0,0 +1,90 @@ +import { Button, Divider, Input, Option, Select, Typography } from '@mui/joy' +import { useContext, useEffect, useState } from 'react' +import { UserContext } from '../../contexts/UserContext' +import { GetUserProfile, UpdateUserDetails } from '../../utils/Fetcher' + +const NotificationSetting = () => { + const { userProfile, setUserProfile } = useContext(UserContext) + useEffect(() => { + if (!userProfile) { + GetUserProfile().then(resp => { + resp.json().then(data => { + setUserProfile(data.res) + setChatID(data.res.chatID) + }) + }) + } + }, []) + const [chatID, setChatID] = useState(userProfile?.chatID) + + return ( + <div className='grid gap-4 py-4' id='notifications'> + <Typography level='h3'>Notification Settings</Typography> + <Divider /> + <Typography level='body-md'>Manage your notification settings</Typography> + + <Select defaultValue='telegram' sx={{ maxWidth: '200px' }} disabled> + <Option value='telegram'>Telegram</Option> + <Option value='discord'>Discord</Option> + </Select> + + <Typography level='body-xs'> + You need to initiate a message to the bot in order for the Telegram + notification to work{' '} + <a + style={{ + textDecoration: 'underline', + color: '#0891b2', + }} + href='https://t.me/DonetickBot' + > + Click here + </a>{' '} + to start a chat + </Typography> + + <Input + value={chatID} + onChange={e => setChatID(e.target.value)} + placeholder='User ID / Chat ID' + sx={{ + width: '200px', + }} + /> + <Typography mt={0} level='body-xs'> + If you don't know your Chat ID, start chat with userinfobot and it will + send you your Chat ID.{' '} + <a + style={{ + textDecoration: 'underline', + color: '#0891b2', + }} + href='https://t.me/userinfobot' + > + Click here + </a>{' '} + to start chat with userinfobot{' '} + </Typography> + + <Button + sx={{ + width: '110px', + mb: 1, + }} + onClick={() => { + UpdateUserDetails({ + chatID: Number(chatID), + }).then(resp => { + resp.json().then(data => { + setUserProfile(data) + }) + }) + }} + > + Save + </Button> + </div> + ) +} + +export default NotificationSetting diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx new file mode 100644 index 0000000..d612eec --- /dev/null +++ b/src/views/Settings/Settings.jsx @@ -0,0 +1,384 @@ +import { + Box, + Button, + Card, + Chip, + CircularProgress, + Container, + Divider, + Input, + Typography, +} from '@mui/joy' +import moment from 'moment' +import { useContext, useEffect, useState } from 'react' +import { UserContext } from '../../contexts/UserContext' +import Logo from '../../Logo' +import { + AcceptCircleMemberRequest, + CancelSubscription, + DeleteCircleMember, + GetAllCircleMembers, + GetCircleMemberRequests, + GetSubscriptionSession, + GetUserCircle, + GetUserProfile, + JoinCircle, + LeaveCircle, +} from '../../utils/Fetcher' +import APITokenSettings from './APITokenSettings' +import NotificationSetting from './NotificationSetting' +import ThemeToggle from './ThemeToggle' + +const Settings = () => { + const { userProfile, setUserProfile } = useContext(UserContext) + const [userCircles, setUserCircles] = useState([]) + const [circleMemberRequests, setCircleMemberRequests] = useState([]) + const [circleInviteCode, setCircleInviteCode] = useState('') + const [circleMembers, setCircleMembers] = useState([]) + useEffect(() => { + GetUserProfile().then(resp => { + resp.json().then(data => { + setUserProfile(data.res) + }) + }) + GetUserCircle().then(resp => { + resp.json().then(data => { + setUserCircles(data.res ? data.res : []) + }) + }) + GetCircleMemberRequests().then(resp => { + resp.json().then(data => { + setCircleMemberRequests(data.res ? data.res : []) + }) + }) + GetAllCircleMembers() + .then(res => res.json()) + .then(data => { + setCircleMembers(data.res ? data.res : []) + }) + }, []) + + useEffect(() => { + const hash = window.location.hash + if (hash) { + const sharingSection = document.getElementById( + window.location.hash.slice(1), + ) + if (sharingSection) { + sharingSection.scrollIntoView({ behavior: 'smooth' }) + } + } + }, []) + + const getSubscriptionDetails = () => { + if (userProfile?.subscription === 'active') { + return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment( + userProfile?.expiration, + ).format('MMM DD, YYYY')}.` + } else if (userProfile?.subscription === 'canceled') { + return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment( + userProfile?.expiration, + ).format('MMM DD, YYYY')}.` + } else { + return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.` + } + } + const getSubscriptionStatus = () => { + if (userProfile?.subscription === 'active') { + return `Plus` + } else if (userProfile?.subscription === 'canceled') { + if (moment().isBefore(userProfile?.expiration)) { + return `Plus(until ${moment(userProfile?.expiration).format( + 'MMM DD, YYYY', + )})` + } + return `Free` + } else { + return `Free` + } + } + + if (userProfile === null) { + return ( + <Container className='flex h-full items-center justify-center'> + <Box className='flex flex-col items-center justify-center'> + <CircularProgress + color='success' + sx={{ '--CircularProgress-size': '200px' }} + > + <Logo /> + </CircularProgress> + </Box> + </Container> + ) + } + return ( + <Container> + <div className='grid gap-4 py-4' id='sharing'> + <Typography level='h3'>Sharing settings</Typography> + <Divider /> + <Typography level='body-md'> + Your account is automatically connected to a Circle when you create or + join one. Easily invite friends by sharing the unique Circle code or + link below. You'll receive a notification below when someone requests + to join your Circle. + </Typography> + <Typography level='title-sm' mb={-1}> + {userCircles[0]?.userRole === 'member' + ? `You part of ${userCircles[0]?.name} ` + : `You circle code is:`} + + <Input + value={userCircles[0]?.invite_code} + disabled + size='lg' + sx={{ + width: '220px', + mb: 1, + }} + /> + <Button + variant='soft' + onClick={() => { + navigator.clipboard.writeText(userCircles[0]?.invite_code) + alert('Code Copied to clipboard') + }} + > + Copy Code + </Button> + <Button + variant='soft' + sx={{ ml: 1 }} + onClick={() => { + navigator.clipboard.writeText( + window.location.protocol + + '//' + + window.location.host + + `/circle/join?code=${userCircles[0]?.invite_code}`, + ) + alert('Link Copied to clipboard') + }} + > + Copy Link + </Button> + {userCircles.length > 0 && userCircles[0]?.userRole === 'member' && ( + <Button + sx={{ ml: 1 }} + onClick={() => { + const confirmed = confirm( + `Are you sure you want to leave your circle?`, + ) + if (confirmed) { + LeaveCircle(userCircles[0]?.id).then(resp => { + if (resp.ok) { + alert('Left circle successfully.') + } else { + alert('Failed to leave circle.') + } + }) + } + }} + > + Leave Circle + </Button> + )} + </Typography> + <Typography level='title-md'>Circle Members</Typography> + {circleMembers.map(member => ( + <Card key={member.id} className='p-4'> + <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> + <Box> + <Typography level='body-md'> + {member.displayName.charAt(0).toUpperCase() + + member.displayName.slice(1)} + {member.userId === userProfile.id ? '(You)' : ''}{' '} + <Chip> + {' '} + {member.isActive ? member.role : 'Pending Approval'} + </Chip> + </Typography> + {member.isActive ? ( + <Typography level='body-sm'> + Joined on {moment(member.createdAt).format('MMM DD, YYYY')} + </Typography> + ) : ( + <Typography level='body-sm' color='danger'> + Request to join{' '} + {moment(member.updatedAt).format('MMM DD, YYYY')} + </Typography> + )} + </Box> + {member.userId !== userProfile.id && member.isActive && ( + <Button + disabled={ + circleMembers.find(m => userProfile.id == m.userId).role !== + 'admin' + } + variant='outlined' + color='danger' + size='sm' + onClick={() => { + const confirmed = confirm( + `Are you sure you want to remove ${member.displayName} from your circle?`, + ) + if (confirmed) { + DeleteCircleMember(member.circleId, member.userId).then( + resp => { + if (resp.ok) { + alert('Removed member successfully.') + } + }, + ) + } + }} + > + Remove + </Button> + )} + </Box> + </Card> + ))} + + {circleMemberRequests.length > 0 && ( + <Typography level='title-md'>Circle Member Requests</Typography> + )} + {circleMemberRequests.map(request => ( + <Card key={request.id} className='p-4'> + <Typography level='body-md'> + {request.displayName} wants to join your circle. + </Typography> + <Button + variant='soft' + color='success' + onClick={() => { + const confirmed = confirm( + `Are you sure you want to accept ${request.displayName}(username:${request.username}) to join your circle?`, + ) + if (confirmed) { + AcceptCircleMemberRequest(request.id).then(resp => { + if (resp.ok) { + alert('Accepted request successfully.') + // reload the page + window.location.reload() + } + }) + } + }} + > + Accept + </Button> + </Card> + ))} + <Divider> or </Divider> + + <Typography level='body-md'> + if want to join someone else's Circle? Ask them for their unique + Circle code or join link. Enter the code below to join their Circle. + </Typography> + + <Typography level='title-sm' mb={-1}> + Enter Circle code: + <Input + placeholder='Enter code' + value={circleInviteCode} + onChange={e => setCircleInviteCode(e.target.value)} + size='lg' + sx={{ + width: '220px', + mb: 1, + }} + /> + <Button + variant='soft' + onClick={() => { + const confirmed = confirm( + `Are you sure you want to leave you circle and join '${circleInviteCode}'?`, + ) + if (confirmed) { + JoinCircle(circleInviteCode).then(resp => { + if (resp.ok) { + alert( + 'Joined circle successfully, wait for the circle owner to accept your request.', + ) + } + }) + } + }} + > + Join Circle + </Button> + </Typography> + </div> + + <div className='grid gap-4 py-4' id='account'> + <Typography level='h3'>Account Settings</Typography> + <Divider /> + <Typography level='body-md'> + Change your account settings, including your password, display name + </Typography> + <Typography level='title-md' mb={-1}> + Account Type : {getSubscriptionStatus()} + </Typography> + <Typography level='body-sm'>{getSubscriptionDetails()}</Typography> + <Box> + <Button + sx={{ + width: '110px', + mb: 1, + }} + disabled={ + userProfile?.subscription === 'active' || + moment(userProfile?.expiration).isAfter(moment()) + } + onClick={() => { + GetSubscriptionSession().then(data => { + data.json().then(data => { + console.log(data) + window.location.href = data.sessionURL + // open in new window: + // window.open(data.sessionURL, '_blank') + }) + }) + }} + > + Upgrade + </Button> + + {userProfile?.subscription === 'active' && ( + <Button + sx={{ + width: '110px', + mb: 1, + ml: 1, + }} + variant='outlined' + onClick={() => { + CancelSubscription().then(resp => { + if (resp.ok) { + alert('Subscription cancelled.') + window.location.reload() + } + }) + }} + > + Cancel + </Button> + )} + </Box> + </div> + <NotificationSetting /> + <APITokenSettings /> + <div className='grid gap-4 py-4'> + <Typography level='h3'>Theme preferences</Typography> + <Divider /> + <Typography level='body-md'> + Choose how the site looks to you. Select a single theme, or sync with + your system and automatically switch between day and night themes. + </Typography> + <ThemeToggle /> + </div> + </Container> + ) +} + +export default Settings diff --git a/src/views/Settings/Sharing.jsx b/src/views/Settings/Sharing.jsx new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/views/Settings/Sharing.jsx diff --git a/src/views/Settings/SharingSettings.jsx b/src/views/Settings/SharingSettings.jsx new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/views/Settings/SharingSettings.jsx diff --git a/src/views/Settings/ThemeToggle.jsx b/src/views/Settings/ThemeToggle.jsx new file mode 100644 index 0000000..6ff33f1 --- /dev/null +++ b/src/views/Settings/ThemeToggle.jsx @@ -0,0 +1,62 @@ +import useStickyState from '@/hooks/useStickyState' +import { + DarkModeOutlined, + LaptopOutlined, + LightModeOutlined, +} from '@mui/icons-material' +import { + Button, + FormControl, + FormLabel, + ToggleButtonGroup, + useColorScheme, +} from '@mui/joy' + +const ELEMENTID = 'select-theme-mode' + +const ThemeToggle = () => { + const { mode, setMode } = useColorScheme() + const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode') + + const handleThemeModeChange = (_, newThemeMode) => { + if (!newThemeMode) return + setThemeMode(newThemeMode) + setMode(newThemeMode) + } + + const FormThemeModeToggleLabel = () => ( + <FormLabel + level='title-md' + id={`${ELEMENTID}-label`} + htmlFor='select-theme-mode' + > + Theme mode + </FormLabel> + ) + + return ( + <FormControl> + <FormThemeModeToggleLabel /> + <div className='flex items-center gap-4'> + <ToggleButtonGroup + id={ELEMENTID} + variant='outlined' + value={themeMode} + onChange={handleThemeModeChange} + > + <Button startDecorator={<LightModeOutlined />} value='light'> + Light + </Button> + <Button startDecorator={<DarkModeOutlined />} value='dark'> + Dark + </Button> + <Button startDecorator={<LaptopOutlined />} value='system'> + System + </Button> + </ToggleButtonGroup> + </div> + </FormControl> + ) +} + +export default ThemeToggle diff --git a/src/views/SummaryCard.jsx b/src/views/SummaryCard.jsx new file mode 100644 index 0000000..ac1d23a --- /dev/null +++ b/src/views/SummaryCard.jsx @@ -0,0 +1,31 @@ +import { Card, IconButton, Typography } from '@mui/joy' + +const SummaryCard = () => { + return ( + <Card> + <div className='flex justify-between'> + <div> + <Typography level='h2'>Summary</Typography> + <Typography level='body-xs'> + This is a summary of your chores + </Typography> + </div> + <IconButton> + <MoreVert /> + </IconButton> + </div> + <div className='flex justify-between'> + <div> + <Typography level='h3'>Due Today</Typography> + <Typography level='h1'>3</Typography> + </div> + <div> + <Typography level='h3'>Overdue</Typography> + <Typography level='h1'>1</Typography> + </div> + </div> + </Card> + ) +} + +export default SummaryCard diff --git a/src/views/Terms/TermsView.jsx b/src/views/Terms/TermsView.jsx new file mode 100644 index 0000000..f0a0d9c --- /dev/null +++ b/src/views/Terms/TermsView.jsx @@ -0,0 +1,194 @@ +import React from 'react' + +const TermsView = () => { + return ( + <div> + <h1>Terms of Service</h1> + + <p> + These Terms of Service ("Terms") govern your access to and use of the + services provided by Favoro LLC, doing business as donetick.com + ("Favoro", "we", "us", or "our"). By accessing or using our website and + services, you agree to be bound by these Terms. If you do not agree to + these Terms, you may not access or use our services. + </p> + + <h2>Use of Services</h2> + + <ul> + <li> + You must be at least 18 years old or have the legal capacity to enter + into contracts in your jurisdiction to use our services. + </li> + <li> + You are responsible for maintaining the confidentiality of your + account credentials and for any activity that occurs under your + account. + </li> + <li> + You may not use our services for any illegal or unauthorized purpose, + or in any way that violates these Terms. + </li> + </ul> + <h2>Subscriptions</h2> + + <ul> + <li> + Some parts of the Service are billed on a subscription basis + ("Subscription(s)"). You will be billed in advance on a recurring and + periodic basis ("Billing Cycle"). Billing cycles are set either on a + monthly or annual basis, depending on the type of subscription plan + you select when purchasing a Subscription. + </li> + <li> + At the end of each Billing Cycle, your Subscription will automatically + renew under the exact same conditions unless you cancel it or Favoro + cancels it. You may cancel your Subscription renewal either through + your online account management page or by contacting Donetickcustomer + support team. + </li> + <li> + A valid payment method, including credit or debit card, is required to + process the payment for your Subscription. You shall provide Favoro + with accurate and complete billing information including full name, + address, state, zip code, telephone number, and a valid payment method + information. By submitting such payment information, you automatically + authorize Donetickto charge all Subscription fees incurred through + your account to any such payment instruments. + </li> + <li> + Should automatic billing fail to occur for any reason, Donetickwill + issue an electronic invoice indicating that you must proceed manually, + within a certain deadline date, with the full payment corresponding to + the billing period as indicated on the invoice. + </li> + </ul> + + <h2>Fee Changes</h2> + <ul> + <li> + {' '} + Favoro, in its sole discretion and at any time, may modify the + Subscription fees for the Subscriptions. Any Subscription fee change + will become effective at the end of the then-current Billing Cycle. + </li> + <li> + Donetickwill provide you with reasonable prior notice of any change in + Subscription fees to give you an opportunity to terminate your + Subscription before such change becomes effective. + </li> + </ul> + + <h2>Refunds</h2> + <ul> + <li> + Certain refund requests for Subscriptions may be considered by Favoro + on a case-by-case basis and granted at the sole discretion of Favoro. + </li> + </ul> + + <h2>Content</h2> + <ul> + <li> + Our services allow you to post, link, store, share, and otherwise make + available certain information, text, graphics, videos, or other + material ("Content"). + </li> + <li> + You are responsible for the Content that you post to our services, + including its legality, reliability, and appropriateness. + </li> + <li> + You may not post Content that is defamatory, obscene, abusive, + offensive, or otherwise objectionable. + </li> + <li> + You may not post Content that violates any party's intellectual + property rights. + </li> + <li> You may not post Content that violates any law or regulation.</li> + </ul> + <h2>Feedback Requests</h2> + + <p> + Our platform allows users to send feedback requests to others. You are + solely responsible for the content of any feedback requests you send + using our services. + </p> + + <p> + You may not use our services to send spam, harass others, or engage in + any abusive behavior. + </p> + + <h2>Credits</h2> + + <p> + Certain actions on our platform may require credits. You can purchase + credits through our website. + </p> + + <p>Credits are non-refundable and non-transferable.</p> + + <h2>Intellectual Property</h2> + + <p> + All content on our website and services, including text, graphics, + logos, and images, is the property of Donetickor its licensors and is + protected by copyright and other intellectual property laws. + </p> + + <p> + You may not reproduce, modify, or distribute any content from our + website or services without our prior written consent. + </p> + + <h2>Disclaimer of Warranties</h2> + + <p> + Our services are provided "as is" and "as available" without any + warranty of any kind, express or implied. + </p> + + <p> + We do not warrant that our services will be uninterrupted, secure, or + error-free, or that any defects will be corrected. + </p> + + <h2>Limitation of Liability</h2> + + <p> + In no event shall Donetickbe liable for any indirect, incidental, + special, consequential, or punitive damages, including but not limited + to lost profits, arising out of or in connection with your use of our + services. + </p> + + <h2>Governing Law</h2> + + <p> + These Terms shall be governed by and construed in accordance with the + laws of the state of [Your State/Country], without regard to its + conflict of law principles. + </p> + + <h2>Changes to These Terms</h2> + + <p> + We may update these Terms from time to time. Any changes will be posted + on this page, and the revised date will be indicated at the top of the + page. Your continued use of our services after any such changes + constitutes your acceptance of the new Terms. + </p> + + <h2>Contact Us</h2> + + <p> + If you have any questions or concerns about these Terms, please contact + us at support@donetick.com + </p> + </div> + ) +} + +export default TermsView diff --git a/src/views/TestView/IconPicker.jsx b/src/views/TestView/IconPicker.jsx new file mode 100644 index 0000000..d1bf229 --- /dev/null +++ b/src/views/TestView/IconPicker.jsx @@ -0,0 +1,58 @@ +import * as allIcons from '@mui/icons-material' // Import all icons using * as +import { Grid, Input, SvgIcon } from '@mui/joy' +import React, { useEffect, useState } from 'react' + +function MuiIconPicker({ onIconSelect }) { + const [searchTerm, setSearchTerm] = useState('') + const [filteredIcons, setFilteredIcons] = useState([]) + const outlined = Object.keys(allIcons).filter(name => + name.includes('Outlined'), + ) + useEffect(() => { + // Filter icons based on the search term + setFilteredIcons( + outlined.filter(name => + name + .toLowerCase() + .includes(searchTerm ? searchTerm.toLowerCase() : false), + ), + ) + }, [searchTerm]) + + const handleIconClick = iconName => { + onIconSelect(iconName) // Callback for selected icon + } + + return ( + <div> + {/* Autocomplete component for searching */} + {JSON.stringify({ 1: searchTerm, filteredIcons: filteredIcons })} + <Input + onChange={(event, newValue) => { + setSearchTerm(newValue) + }} + /> + {/* Grid to display icons */} + <Grid container spacing={2}> + {filteredIcons.map(iconName => { + const IconComponent = allIcons[iconName] + if (IconComponent) { + // Add this check to prevent errors + return ( + <Grid item key={iconName} xs={3} sm={2} md={1}> + <SvgIcon + component={IconComponent} + onClick={() => handleIconClick(iconName)} + style={{ cursor: 'pointer' }} + /> + </Grid> + ) + } + return null // Return null for non-icon exports + })} + </Grid> + </div> + ) +} + +export default MuiIconPicker diff --git a/src/views/TestView/Test.jsx b/src/views/TestView/Test.jsx new file mode 100644 index 0000000..0494e93 --- /dev/null +++ b/src/views/TestView/Test.jsx @@ -0,0 +1,11 @@ +import MuiIconPicker from './IconPicker' + +const TestView = () => { + return ( + <div> + <MuiIconPicker /> + </div> + ) +} + +export default TestView diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx new file mode 100644 index 0000000..39f0e30 --- /dev/null +++ b/src/views/Things/ThingsHistory.jsx @@ -0,0 +1,13 @@ +import { Container, Typography } from '@mui/joy' + +const ThingsHistory = () => { + return ( + <Container maxWidth='md'> + <Typography level='h3' mb={1.5}> + Summary: + </Typography> + </Container> + ) +} + +export default ThingsHistory diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx new file mode 100644 index 0000000..deb2df5 --- /dev/null +++ b/src/views/Things/ThingsView.jsx @@ -0,0 +1,324 @@ +import { + Add, + Delete, + Edit, + Flip, + PlusOne, + ToggleOff, + ToggleOn, + Widgets, +} from '@mui/icons-material' +import { + Box, + Card, + Chip, + Container, + Grid, + IconButton, + Typography, +} from '@mui/joy' +import { useEffect, useState } from 'react' +import { + CreateThing, + DeleteThing, + GetThings, + SaveThing, + UpdateThingState, +} from '../../utils/Fetcher' +import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import CreateThingModal from '../Modals/Inputs/CreateThingModal' + +const ThingCard = ({ + thing, + onEditClick, + onStateChangeRequest, + onDeleteClick, +}) => { + const getThingIcon = type => { + if (type === 'text') { + return <Flip /> + } else if (type === 'number') { + return <PlusOne /> + } else if (type === 'boolean') { + if (thing.state === 'true') { + return <ToggleOn /> + } else { + return <ToggleOff /> + } + } else { + return <ToggleOff /> + } + } + return ( + <Card + variant='outlined' + sx={{ + // display: 'flex', + // flexDirection: 'row', // Change to 'row' + justifyContent: 'space-between', + p: 2, + backgroundColor: 'white', + boxShadow: 'sm', + borderRadius: 8, + mb: 1, + }} + > + <Grid container> + <Grid item xs={9}> + <Box + sx={{ + display: 'flex', + flexDirection: 'row', + gap: 1, + }} + > + <Typography level='title-lg' component='h2'> + {thing?.name} + </Typography> + <Chip level='body-md' component='p'> + {thing?.type} + </Chip> + </Box> + <Box> + <Typography level='body-sm' component='p'> + Current state: + <Chip level='title-md' component='span' size='sm'> + {thing?.state} + </Chip> + </Typography> + </Box> + </Grid> + <Grid item xs={3}> + <Box display='flex' justifyContent='flex-end' alignItems='flex-end'> + {/* <ButtonGroup> */} + <IconButton + variant='solid' + color='success' + onClick={() => { + onStateChangeRequest(thing) + }} + sx={{ + borderRadius: '50%', + width: 50, + height: 50, + zIndex: 1, + }} + > + {getThingIcon(thing?.type)} + </IconButton> + <IconButton + // sx={{ width: 15 }} + variant='soft' + color='success' + onClick={() => { + onEditClick(thing) + }} + sx={{ + borderRadius: '50%', + width: 25, + height: 25, + position: 'relative', + left: -10, + }} + > + <Edit /> + </IconButton> + {/* add delete icon: */} + <IconButton + // sx={{ width: 15 }} + + color='danger' + variant='soft' + onClick={() => { + onDeleteClick(thing) + }} + sx={{ + borderRadius: '50%', + width: 25, + height: 25, + position: 'relative', + left: -10, + }} + > + <Delete /> + </IconButton> + </Box> + </Grid> + </Grid> + </Card> + ) +} + +const ThingsView = () => { + const [things, setThings] = useState([]) + const [isShowCreateThingModal, setIsShowCreateThingModal] = useState(false) + const [createModalThing, setCreateModalThing] = useState(null) + const [confirmModelConfig, setConfirmModelConfig] = useState({}) + useEffect(() => { + // fetch things + GetThings().then(result => { + result.json().then(data => { + setThings(data.res) + }) + }) + }, []) + + const handleSaveThing = thing => { + let saveFunc = CreateThing + if (thing?.id) { + saveFunc = SaveThing + } + saveFunc(thing).then(result => { + result.json().then(data => { + if (thing?.id) { + const currentThings = [...things] + const thingIndex = currentThings.findIndex( + currentThing => currentThing.id === thing.id, + ) + currentThings[thingIndex] = data.res + setThings(currentThings) + } else { + const currentThings = [...things] + currentThings.push(data.res) + setThings(currentThings) + } + }) + }) + } + const handleEditClick = thing => { + setCreateModalThing(thing) + setIsShowCreateThingModal(true) + } + const handleDeleteClick = thing => { + setConfirmModelConfig({ + isOpen: true, + title: 'Delete Things', + confirmText: 'Delete', + cancelText: 'Cancel', + message: 'Are you sure you want to delete this Thing?', + onClose: isConfirmed => { + if (isConfirmed === true) { + DeleteThing(thing.id).then(response => { + if (response.ok) { + const currentThings = [...things] + const thingIndex = currentThings.findIndex( + currentThing => currentThing.id === thing.id, + ) + currentThings.splice(thingIndex, 1) + setThings(currentThings) + } + }) + } + setConfirmModelConfig({}) + }, + }) + } + + const handleStateChangeRequest = thing => { + if (thing?.type === 'text') { + setCreateModalThing(thing) + setIsShowCreateThingModal(true) + } else { + if (thing?.type === 'number') { + thing.state = Number(thing.state) + 1 + } else if (thing?.type === 'boolean') { + if (thing.state === 'true') { + thing.state = 'false' + } else { + thing.state = 'true' + } + } + + UpdateThingState(thing).then(result => { + result.json().then(data => { + const currentThings = [...things] + const thingIndex = currentThings.findIndex( + currentThing => currentThing.id === thing.id, + ) + currentThings[thingIndex] = data.res + setThings(currentThings) + }) + }) + } + } + + return ( + <Container maxWidth='md'> + {things.length === 0 && ( + <Box + sx={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + height: '50vh', + }} + > + <Widgets + sx={{ + fontSize: '4rem', + // color: 'text.disabled', + mb: 1, + }} + /> + <Typography level='title-md' gutterBottom> + No things has been created/found + </Typography> + </Box> + )} + {things.map(thing => ( + <ThingCard + key={thing?.id} + thing={thing} + onEditClick={handleEditClick} + onDeleteClick={handleDeleteClick} + onStateChangeRequest={handleStateChangeRequest} + /> + ))} + <Box + // variant='outlined' + sx={{ + position: 'fixed', + bottom: 0, + left: 10, + p: 2, // padding + display: 'flex', + justifyContent: 'flex-end', + gap: 2, + + 'z-index': 1000, + }} + > + <IconButton + color='primary' + variant='solid' + sx={{ + borderRadius: '50%', + width: 50, + height: 50, + }} + // startDecorator={<Add />} + onClick={() => { + setIsShowCreateThingModal(true) + }} + > + <Add /> + </IconButton> + {isShowCreateThingModal && ( + <CreateThingModal + isOpen={isShowCreateThingModal} + onClose={() => { + setIsShowCreateThingModal(false) + setCreateModalThing(null) + }} + onSave={handleSaveThing} + currentThing={createModalThing} + /> + )} + <ConfirmationModal config={confirmModelConfig} /> + </Box> + </Container> + ) +} + +export default ThingsView diff --git a/src/views/components/AutocompleteSelect.jsx b/src/views/components/AutocompleteSelect.jsx new file mode 100644 index 0000000..7708214 --- /dev/null +++ b/src/views/components/AutocompleteSelect.jsx @@ -0,0 +1,87 @@ +import Add from '@mui/icons-material/Add' +import Autocomplete, { createFilterOptions } from '@mui/joy/Autocomplete' +import AutocompleteOption from '@mui/joy/AutocompleteOption' +import FormControl from '@mui/joy/FormControl' +import ListItemDecorator from '@mui/joy/ListItemDecorator' +import * as React from 'react' + +const filter = createFilterOptions() + +export default function FreeSoloCreateOption({ options, onSelectChange }) { + React.useEffect(() => { + setValue(options) + }, [options]) + + const [value, setValue] = React.useState([]) + const [selectOptions, setSelectOptions] = React.useState( + options ? options : [], + ) + return ( + <FormControl id='free-solo-with-text-demo'> + <Autocomplete + value={value} + multiple + size='lg' + on + onChange={(event, newValue) => { + if (typeof newValue === 'string') { + setValue({ + title: newValue, + }) + } else if (newValue && newValue.inputValue) { + // Create a new value from the user input + setValue({ + title: newValue.inputValue, + }) + } else { + setValue(newValue) + } + onSelectChange(newValue) + }} + filterOptions={(options, params) => { + const filtered = filter(options, params) + + const { inputValue } = params + // Suggest the creation of a new value + const isExisting = options.some(option => inputValue === option.title) + if (inputValue !== '' && !isExisting) { + filtered.push({ + inputValue, + title: `Add "${inputValue}"`, + }) + } + + return filtered + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + // freeSolo + options={selectOptions} + getOptionLabel={option => { + // Value selected with enter, right from the input + if (typeof option === 'string') { + return option + } + // Add "xxx" option created dynamically + if (option.inputValue) { + return option.inputValue + } + // Regular option + return option.title + }} + renderOption={(props, option) => ( + <AutocompleteOption {...props}> + {option.title?.startsWith('Add "') && ( + <ListItemDecorator> + <Add /> + </ListItemDecorator> + )} + + {option.title ? option.title : option} + </AutocompleteOption> + )} + /> + </FormControl> + ) +} diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx new file mode 100644 index 0000000..25463b2 --- /dev/null +++ b/src/views/components/NavBar.jsx @@ -0,0 +1,177 @@ +import Logo from '@/assets/logo.svg' +import { + AccountBox, + HomeOutlined, + ListAltRounded, + Logout, + MenuRounded, + Message, + SettingsOutlined, + ShareOutlined, + Widgets, +} from '@mui/icons-material' +import { + Box, + Drawer, + IconButton, + List, + ListItemButton, + ListItemContent, + ListItemDecorator, + Typography, +} from '@mui/joy' +import { useState } from 'react' +import { useLocation } from 'react-router-dom' +import { version } from '../../../package.json' +import NavBarLink from './NavBarLink' +const links = [ + { + to: '/my/chores', + label: 'Home', + icon: <HomeOutlined />, + }, + { + to: '/chores', + label: 'Desktop View', + icon: <ListAltRounded />, + }, + { + to: '/things', + label: 'Things', + icon: <Widgets />, + }, + { + to: '/settings#sharing', + label: 'Sharing', + icon: <ShareOutlined />, + }, + { + to: '/settings#notifications', + label: 'Notifications', + icon: <Message />, + }, + { + to: '/settings#account', + label: 'Account', + icon: <AccountBox />, + }, + { + to: '/settings', + label: 'Settings', + icon: <SettingsOutlined />, + }, +] + +const NavBar = () => { + const [drawerOpen, setDrawerOpen] = useState(false) + const [openDrawer, closeDrawer] = [ + () => setDrawerOpen(true), + () => setDrawerOpen(false), + ] + const location = useLocation() + // if url has /landing then remove the navbar: + if ( + ['/', '/signup', '/login', '/landing', '/forgot-password'].includes( + location.pathname, + ) + ) { + return null + } + + return ( + <nav className='flex gap-2 p-3'> + <IconButton size='sm' variant='plain' onClick={() => setDrawerOpen(true)}> + <MenuRounded /> + </IconButton> + <Box className='flex items-center gap-2'> + <img component='img' src={Logo} width='34' /> + <Typography + level='title-lg' + sx={{ + fontWeight: 700, + fontSize: 24, + }} + > + Done + <span + style={{ + color: '#06b6d4', + fontWeight: 600, + }} + > + tick✓ + </span> + </Typography> + </Box> + <Drawer + open={drawerOpen} + onClose={closeDrawer} + size='sm' + onClick={closeDrawer} + > + <div> + {/* <div className='align-center flex px-5 pt-4'> + <ModalClose size='sm' sx={{ top: 'unset', right: 20 }} /> + </div> */} + <List + // sx={{ p: 2, height: 'min-content' }} + size='md' + onClick={openDrawer} + sx={{ borderRadius: 4, width: '100%', padding: 1 }} + > + {links.map((link, index) => ( + <NavBarLink key={index} link={link} /> + ))} + </List> + </div> + <div> + <List + sx={{ + p: 2, + height: 'min-content', + position: 'absolute', + bottom: 0, + borderRadius: 4, + width: '100%', + padding: 2, + }} + size='md' + onClick={openDrawer} + > + <ListItemButton + onClick={() => { + localStorage.removeItem('ca_token') + localStorage.removeItem('ca_expiration') + // go to login page: + window.location.href = '/login' + }} + sx={{ + py: 1.2, + }} + > + <ListItemDecorator> + <Logout /> + </ListItemDecorator> + <ListItemContent>Logout</ListItemContent> + </ListItemButton> + <Typography + level='body-xs' + sx={{ + // p: 2, + p: 1, + color: 'text.tertiary', + textAlign: 'center', + bottom: 0, + // mb: -2, + }} + > + V{version} + </Typography> + </List> + </div> + </Drawer> + </nav> + ) +} + +export default NavBar diff --git a/src/views/components/NavBarLink.jsx b/src/views/components/NavBarLink.jsx new file mode 100644 index 0000000..3fe10d2 --- /dev/null +++ b/src/views/components/NavBarLink.jsx @@ -0,0 +1,31 @@ +import { + ListItem, + ListItemButton, + ListItemContent, + ListItemDecorator, +} from '@mui/joy' +import { Link } from 'react-router-dom' + +const NavBarLink = ({ link }) => { + const { to, icon, label } = link + return ( + <ListItem> + <ListItemButton + key={to} + component={Link} + to={to} + variant='plain' + color='neutral' + sx={{ + borderRadius: 4, + py: 1.2, + }} + > + <ListItemDecorator>{icon}</ListItemDecorator> + <ListItemContent>{label}</ListItemContent> + </ListItemButton> + </ListItem> + ) +} + +export default NavBarLink diff --git a/src/views/components/NavBarMobile.jsx b/src/views/components/NavBarMobile.jsx new file mode 100644 index 0000000..5fb1100 --- /dev/null +++ b/src/views/components/NavBarMobile.jsx @@ -0,0 +1,107 @@ +import * as React from 'react' +import Box from '@mui/joy/Box' +import ListItemDecorator from '@mui/joy/ListItemDecorator' +import Tabs from '@mui/joy/Tabs' +import TabList from '@mui/joy/TabList' +import Tab, { tabClasses } from '@mui/joy/Tab' +import HomeRoundedIcon from '@mui/icons-material/HomeRounded' +import FavoriteBorder from '@mui/icons-material/FavoriteBorder' +import Search from '@mui/icons-material/Search' +import Person from '@mui/icons-material/Person' + +export default function NavBarMobile() { + const [index, setIndex] = React.useState(0) + const colors = ['primary', 'danger', 'success', 'warning'] + return ( + <Box + sx={{ + position: 'absolute', + width: '100%', + bottom: 0, + + flexGrow: 1, + + p: 1, + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px', + + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} + > + <Tabs + size='lg' + aria-label='Bottom Navigation' + value={index} + onChange={(event, value) => setIndex(value)} + sx={theme => ({ + p: 1, + borderRadius: 16, + maxWidth: 500, + // mx: 'auto', + boxShadow: theme.shadow.sm, + '--joy-shadowChannel': theme.vars.palette[colors[index]].darkChannel, + [`& .${tabClasses.root}`]: { + py: 1, + flex: 1, + transition: '0.3s', + fontWeight: 'md', + fontSize: 'md', + [`&:not(.${tabClasses.selected}):not(:hover)`]: { + opacity: 0.7, + }, + }, + })} + > + <TabList + variant='plain' + size='sm' + disableUnderline + sx={{ borderRadius: 'lg', p: 0 }} + > + <Tab + disableIndicator + orientation='vertical' + {...(index === 0 && { color: colors[0] })} + > + <ListItemDecorator> + <HomeRoundedIcon /> + </ListItemDecorator> + Home + </Tab> + <Tab + disableIndicator + orientation='vertical' + {...(index === 1 && { color: colors[1] })} + > + <ListItemDecorator> + <FavoriteBorder /> + </ListItemDecorator> + Likes + </Tab> + <Tab + disableIndicator + orientation='vertical' + {...(index === 2 && { color: colors[2] })} + > + <ListItemDecorator> + <Search /> + </ListItemDecorator> + Search + </Tab> + <Tab + disableIndicator + orientation='vertical' + {...(index === 3 && { color: colors[3] })} + > + <ListItemDecorator> + <Person /> + </ListItemDecorator> + Profile + </Tab> + </TabList> + </Tabs> + </Box> + ) +} |