Compare commits
588 Commits
before-ref
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
31ca6f4a78 | |
|
|
1f9bc3a2d3 | |
|
|
aad81bedbd | |
|
|
1158c5fc1d | |
|
|
a9727b990d | |
|
|
a9f94a2cfa | |
|
|
eb0b9bfc40 | |
|
|
b4e1a99add | |
|
|
c21d3ff504 | |
|
|
2957d95133 | |
|
|
d6ce727b0c | |
|
|
c18c9fadd1 | |
|
|
dce33716d9 | |
|
|
cf8ba61943 | |
|
|
26383aa79d | |
|
|
136bd77d5f | |
|
|
ba6620f040 | |
|
|
4a8e7b7dc8 | |
|
|
6ca48eb537 | |
|
|
2864a098ab | |
|
|
fec0cceba6 | |
|
|
3bf39ce1b0 | |
|
|
3a047b5bbe | |
|
|
50145d4d1a | |
|
|
4df2e8a48b | |
|
|
3dae451776 | |
|
|
4d038f37b3 | |
|
|
a09ef4df65 | |
|
|
628862d024 | |
|
|
af83be1fcc | |
|
|
05cb5527e3 | |
|
|
7d910a0f75 | |
|
|
ab74f6426e | |
|
|
390bb8ff67 | |
|
|
fcd108f9e7 | |
|
|
dd2c6ee3d2 | |
|
|
9917425cc8 | |
|
|
3f54ca11a1 | |
|
|
e059f800d6 | |
|
|
f2dec10c05 | |
|
|
d8117eac7d | |
|
|
354514818c | |
|
|
4ac814be93 | |
|
|
2f7387a67f | |
|
|
1cf779c5a6 | |
|
|
64c9a75d92 | |
|
|
5a8c4e6a9b | |
|
|
2fd728b5e8 | |
|
|
9bda1672d5 | |
|
|
101eade38f | |
|
|
6a1c8bcd75 | |
|
|
c49a09e721 | |
|
|
d526ded04b | |
|
|
06344bee72 | |
|
|
f9f67ff9ea | |
|
|
9e519166d5 | |
|
|
1b649bbe96 | |
|
|
9ed305c08b | |
|
|
dfebfa20a2 | |
|
|
564c81362e | |
|
|
d938ea3ef1 | |
|
|
beceb476d9 | |
|
|
ca882e4568 | |
|
|
227c626c3a | |
|
|
c3606e3c3e | |
|
|
ae9ffccf65 | |
|
|
897dcb8c3b | |
|
|
38e1fdeeda | |
|
|
c94e587675 | |
|
|
5354dad9fe | |
|
|
cbbab432e4 | |
|
|
e21ab82729 | |
|
|
987a98732a | |
|
|
3ddfda80ed | |
|
|
3e5da523ce | |
|
|
6f6625d3db | |
|
|
e9b4331d60 | |
|
|
b7e5b70df7 | |
|
|
ec04a20b8d | |
|
|
7b51b8db72 | |
|
|
fddb08554d | |
|
|
14116c24b8 | |
|
|
9307cf6259 | |
|
|
39de752084 | |
|
|
d50783bb7f | |
|
|
2f22563d8d | |
|
|
a0c7c134d3 | |
|
|
e8d08572c6 | |
|
|
0191bde3e6 | |
|
|
5c738d8f4d | |
|
|
6ca8817108 | |
|
|
b9f5c0b40e | |
|
|
c89cfec0c8 | |
|
|
85840a9fa6 | |
|
|
a9ff99d48e | |
|
|
160fdaef88 | |
|
|
c020a739c4 | |
|
|
3b6e34fed8 | |
|
|
e4d09f118d | |
|
|
662adb7bda | |
|
|
9c479f4462 | |
|
|
447da26e16 | |
|
|
9878e59a1b | |
|
|
af0530c764 | |
|
|
30067567ab | |
|
|
501509f4df | |
|
|
b0edfdc976 | |
|
|
4745ed2242 | |
|
|
4112828108 | |
|
|
17e5edc17d | |
|
|
24a0f6f6c4 | |
|
|
76975298f0 | |
|
|
770ae6d5cf | |
|
|
25fdb28540 | |
|
|
6a026e0039 | |
|
|
edb8cd0ba7 | |
|
|
0908fcab89 | |
|
|
ceb18f613f | |
|
|
72776eee35 | |
|
|
0cf67aa8bb | |
|
|
1893aef144 | |
|
|
d2806e7fe0 | |
|
|
174e4be581 | |
|
|
583b47bf2f | |
|
|
b623363369 | |
|
|
2b72adbfed | |
|
|
2b1f1a514d | |
|
|
af452c014d | |
|
|
53cfa8deee | |
|
|
adefb7de07 | |
|
|
d17ba78413 | |
|
|
56419385c3 | |
|
|
4b55018014 | |
|
|
449b4157c3 | |
|
|
cad0113d59 | |
|
|
4e98fd7839 | |
|
|
366eb9d0dd | |
|
|
ed6cb9e00d | |
|
|
9267c05a9b | |
|
|
e2d574eb5d | |
|
|
658fe92f9e | |
|
|
1525572c03 | |
|
|
90b548973b | |
|
|
fa188868b1 | |
|
|
719ee3456a | |
|
|
afc136d4f9 | |
|
|
802ceb1ff5 | |
|
|
adf9aac96a | |
|
|
1898ff0813 | |
|
|
e97ada9654 | |
|
|
1f79a454c4 | |
|
|
029493f3ed | |
|
|
882332fc55 | |
|
|
a318765143 | |
|
|
bafed33c1f | |
|
|
43451bdaff | |
|
|
a2c9b59a1e | |
|
|
164cb11cf3 | |
|
|
84fab1da37 | |
|
|
9843fb0144 | |
|
|
8b23750641 | |
|
|
3d775da19f | |
|
|
ec0880ade3 | |
|
|
58b6765a71 | |
|
|
bf28e81613 | |
|
|
c862b5f657 | |
|
|
a4cb4b0107 | |
|
|
78221abba4 | |
|
|
b37663d17e | |
|
|
e063ba4b65 | |
|
|
458fbc836b | |
|
|
e3cfb240d0 | |
|
|
fcedde96bb | |
|
|
4cb8f3fc66 | |
|
|
eca8898d3b | |
|
|
4e770b7aec | |
|
|
0a3dea0edb | |
|
|
69ccaf85be | |
|
|
40ed8505ed | |
|
|
1c8bf6d539 | |
|
|
fa4712aa34 | |
|
|
662291458f | |
|
|
e8ca1f927b | |
|
|
017d83c060 | |
|
|
528fe8bdf1 | |
|
|
c54a1b7abc | |
|
|
b978a7f64e | |
|
|
2ac23c86ea | |
|
|
6b56945eb0 | |
|
|
aca83fae6d | |
|
|
5850ff923f | |
|
|
2329d24ce3 | |
|
|
8b0cf0b6cf | |
|
|
128f88c99d | |
|
|
261d06cf5a | |
|
|
856fd37297 | |
|
|
e2a3e6bf05 | |
|
|
0732382216 | |
|
|
0ae080130b | |
|
|
fe12874400 | |
|
|
4e6aa1dc93 | |
|
|
20fbd97b01 | |
|
|
94eea071b6 | |
|
|
d9e50dc4f9 | |
|
|
3b29dd616c | |
|
|
1a61830d7a | |
|
|
7c3f88c2bf | |
|
|
50da3e9504 | |
|
|
772010458c | |
|
|
6205c26675 | |
|
|
6712ea9ad8 | |
|
|
41ece67739 | |
|
|
268837a62f | |
|
|
512c3ae318 | |
|
|
27d1ef6c9b | |
|
|
18fe23f92f | |
|
|
8f7676a36c | |
|
|
e4be7ef344 | |
|
|
aa518b9eab | |
|
|
0155b855de | |
|
|
1af5427db4 | |
|
|
9ac59e5e72 | |
|
|
c43caa8a00 | |
|
|
aff1da2fe4 | |
|
|
e8f5d8a663 | |
|
|
f3bd318635 | |
|
|
9e965e1799 | |
|
|
9b5a6191d6 | |
|
|
8f2939d958 | |
|
|
33b484a405 | |
|
|
62ffdc3ab6 | |
|
|
7373e083a1 | |
|
|
57e7da6068 | |
|
|
b34aca68ec | |
|
|
3c65ba210f | |
|
|
fb0974b0ff | |
|
|
6bcf4ef96e | |
|
|
aab750f93b | |
|
|
167f119163 | |
|
|
e1b9dd6dc6 | |
|
|
3d581e6ed2 | |
|
|
95651a9d12 | |
|
|
7490bf3ea1 | |
|
|
3c2a6f76cc | |
|
|
62a0deba57 | |
|
|
bcb581af03 | |
|
|
78da65706d | |
|
|
8155ed3207 | |
|
|
eed383522b | |
|
|
55068ca6ca | |
|
|
0fee30ab26 | |
|
|
f820124db9 | |
|
|
0428ba5e5c | |
|
|
3ff09c65fc | |
|
|
b09c41c353 | |
|
|
d9f48330f2 | |
|
|
c97c2e93c6 | |
|
|
205176caab | |
|
|
c62cb76347 | |
|
|
a85eba776a | |
|
|
95282552a5 | |
|
|
93c584efc7 | |
|
|
b7f16f31d9 | |
|
|
012d8c81f5 | |
|
|
b4c03fc922 | |
|
|
3d2ff24531 | |
|
|
9d56f4a1b2 | |
|
|
fcc726046b | |
|
|
a0f06ac704 | |
|
|
4542021a34 | |
|
|
b32cb1eede | |
|
|
b41d1b1309 | |
|
|
fe9bda3ab0 | |
|
|
6cc5f311fc | |
|
|
4d6529d6d1 | |
|
|
97fa10fc55 | |
|
|
e0adf39626 | |
|
|
d4b27cf545 | |
|
|
64a1cb27c2 | |
|
|
c03248de48 | |
|
|
17cb1075cf | |
|
|
787560916b | |
|
|
06828c2d8b | |
|
|
baf6fbbb2b | |
|
|
5b26f6b203 | |
|
|
423eb1ed7b | |
|
|
0142f55f09 | |
|
|
8fd467b1d0 | |
|
|
198632f94f | |
|
|
7c885ae2e6 | |
|
|
36284e507d | |
|
|
e6e4969695 | |
|
|
370a8622c3 | |
|
|
93e0346538 | |
|
|
d657a08d94 | |
|
|
9eab89a72b | |
|
|
32267b766a | |
|
|
b80ca1d852 | |
|
|
5cb179b055 | |
|
|
70ebc7b2ce | |
|
|
25eb5c845d | |
|
|
d3d4e95a19 | |
|
|
357cf192ac | |
|
|
40aacdd809 | |
|
|
251a894262 | |
|
|
bec6434f73 | |
|
|
b294c09e1a | |
|
|
62ec4aae1f | |
|
|
8f20d6e43c | |
|
|
4cc8cc225c | |
|
|
508a60ab2a | |
|
|
92f1389680 | |
|
|
5368193ad0 | |
|
|
fd7fff3fb6 | |
|
|
b0056b66b8 | |
|
|
a2b9b065d0 | |
|
|
fb414b02d8 | |
|
|
946459a104 | |
|
|
415bd3b091 | |
|
|
05a3f37a6e | |
|
|
ef6fa75486 | |
|
|
d755218f5f | |
|
|
f61fbcc44f | |
|
|
ded7785170 | |
|
|
eb084c8a8c | |
|
|
66bcef28c6 | |
|
|
bfa0c4d9eb | |
|
|
2ed1c99435 | |
|
|
3aa171aef8 | |
|
|
1312ff142c | |
|
|
4d26b88153 | |
|
|
4bbc210ef1 | |
|
|
6b8e921cc4 | |
|
|
09c663b11c | |
|
|
8036ffc595 | |
|
|
446e2e5970 | |
|
|
d69598bd56 | |
|
|
af18b825fe | |
|
|
f5ccd6e4b7 | |
|
|
fd1561de07 | |
|
|
1e28ed8f25 | |
|
|
f90dae3f96 | |
|
|
08098584b3 | |
|
|
efef5f932f | |
|
|
fe5fa2eecd | |
|
|
c03a5cc434 | |
|
|
49ee30bcf1 | |
|
|
1b86bd7912 | |
|
|
cd89395cf1 | |
|
|
b129dc5cc6 | |
|
|
58b81e6cf4 | |
|
|
38ec23f4fa | |
|
|
b767fb4aca | |
|
|
9c60f4d228 | |
|
|
2e9d82bc31 | |
|
|
413dc91a48 | |
|
|
184578fcc4 | |
|
|
405ac1c0bd | |
|
|
acd8ddb7bb | |
|
|
e8b8a195bc | |
|
|
d384ef2d59 | |
|
|
f7a38f55b9 | |
|
|
afae0ea8fb | |
|
|
dd727b3fac | |
|
|
2a2067cd0f | |
|
|
f533eb0b05 | |
|
|
36a628e5de | |
|
|
096bf67079 | |
|
|
9254a6d1da | |
|
|
dfddb2b3af | |
|
|
fcd385b2b0 | |
|
|
5b2e59ed55 | |
|
|
ee3e5ac150 | |
|
|
e74c33a88b | |
|
|
2f28609cd5 | |
|
|
055d97075d | |
|
|
ab9cc5aab3 | |
|
|
0e83ea4dd1 | |
|
|
e8e5b06a91 | |
|
|
9fca36b281 | |
|
|
0e735102ff | |
|
|
44fe38a5d9 | |
|
|
80d24021ef | |
|
|
ce2cf6d7a8 | |
|
|
f30aeb1d20 | |
|
|
de8720ee60 | |
|
|
4c1b0b3e1f | |
|
|
545626a94e | |
|
|
b54c72c074 | |
|
|
d0a66c605e | |
|
|
44833471da | |
|
|
8fc0b33189 | |
|
|
35fad3ef5c | |
|
|
fb26280a78 | |
|
|
e115ebc208 | |
|
|
5ad9c7e4fc | |
|
|
ae21729bd7 | |
|
|
6e750ff63b | |
|
|
26f45e7602 | |
|
|
4f87a5c2cd | |
|
|
311edd0dce | |
|
|
7c18fa9002 | |
|
|
86ee569794 | |
|
|
2e71f596c1 | |
|
|
5b367b61c9 | |
|
|
aebd647d35 | |
|
|
cd5c5af94f | |
|
|
b897b513b8 | |
|
|
2cb1888f8d | |
|
|
fa4e08c8e2 | |
|
|
84bc94538f | |
|
|
faac196af7 | |
|
|
3be0520b60 | |
|
|
8945082220 | |
|
|
ada458bac6 | |
|
|
e479093746 | |
|
|
92adf9c8d5 | |
|
|
641577665f | |
|
|
e5de0b5f45 | |
|
|
43b29fbecc | |
|
|
c50eee237c | |
|
|
2fa2ed62ba | |
|
|
205f1bef51 | |
|
|
7af202a198 | |
|
|
03b4318c7a | |
|
|
b2a3fe4ce8 | |
|
|
648a6062a9 | |
|
|
5047a0ddbf | |
|
|
60269f2ada | |
|
|
3d0daa6d60 | |
|
|
063b147448 | |
|
|
6094a6c2fd | |
|
|
976a1260b2 | |
|
|
09b685ca86 | |
|
|
99a1812b35 | |
|
|
73861d6a18 | |
|
|
b7e775f854 | |
|
|
9468a589fc | |
|
|
af028923e5 | |
|
|
9d46b7c195 | |
|
|
b42975ac30 | |
|
|
91ff195ab8 | |
|
|
855835af53 | |
|
|
0c272532b3 | |
|
|
74c823edeb | |
|
|
8e624fdb49 | |
|
|
cb66cdff20 | |
|
|
abcc81649c | |
|
|
737ade5c46 | |
|
|
0be99d845d | |
|
|
4298b90a6f | |
|
|
23162bb324 | |
|
|
24d7f83394 | |
|
|
c722103b39 | |
|
|
ea00b664a3 | |
|
|
12facb0ecd | |
|
|
6afc119adf | |
|
|
88aac37021 | |
|
|
b9319e801b | |
|
|
25aa59c067 | |
|
|
6d943da96e | |
|
|
174525550c | |
|
|
e37d73c7ac | |
|
|
5168520efb | |
|
|
5caf77d0c3 | |
|
|
15d181a415 | |
|
|
81aca317fc | |
|
|
0b9ac1206d | |
|
|
c95361d0f0 | |
|
|
d2a439c04e | |
|
|
b50417f1a0 | |
|
|
ae1f934b39 | |
|
|
df20030b9f | |
|
|
091bffcb52 | |
|
|
11d81a212e | |
|
|
5b5239d18d | |
|
|
f48f59d1a7 | |
|
|
8f6c48ad0a | |
|
|
3921ee099c | |
|
|
8d6ad16498 | |
|
|
0b397ac36a | |
|
|
d472987c2c | |
|
|
45b597d9ff | |
|
|
59cdb70f9e | |
|
|
09f17d60f8 | |
|
|
5b45f88281 | |
|
|
8802b797d1 | |
|
|
b5814cdb1f | |
|
|
f08e1bce8e | |
|
|
224f2bfd51 | |
|
|
18b9b8d0e2 | |
|
|
f045304019 | |
|
|
30cc932ef9 | |
|
|
a49eab3955 | |
|
|
5326bab255 | |
|
|
684f9c791c | |
|
|
683c53838b | |
|
|
e136eb090c | |
|
|
133963766a | |
|
|
77082119cc | |
|
|
31f4a4a37e | |
|
|
357a957afd | |
|
|
fdc2c8a554 | |
|
|
f5c87c1aeb | |
|
|
317cf4cb11 | |
|
|
0f3e71300f | |
|
|
5db767254c | |
|
|
e5503fcff6 | |
|
|
0b0e420a5a | |
|
|
70e24b3d0e | |
|
|
3b48625597 | |
|
|
f499c818da | |
|
|
4610f14394 | |
|
|
313526207f | |
|
|
03d1fca234 | |
|
|
3c4acae3bd | |
|
|
10c38506fa | |
|
|
ba1cab4fcb | |
|
|
0a5e13dc04 | |
|
|
d40df8622c | |
|
|
9672e6b7f0 | |
|
|
578e33eb4b | |
|
|
a036070a33 | |
|
|
95e0cf5918 | |
|
|
02a7dfdde7 | |
|
|
c8a1f9c3b8 | |
|
|
739756e553 | |
|
|
23eed87017 | |
|
|
d633557d30 | |
|
|
dce00ac8b2 | |
|
|
b017014c60 | |
|
|
9352424aa7 | |
|
|
15a86e4830 | |
|
|
86b3cec1e7 | |
|
|
e38c6b4916 | |
|
|
3acdfa09ae | |
|
|
1f5324a975 | |
|
|
10d1ea5983 | |
|
|
a7605b589c | |
|
|
e5945e48dd | |
|
|
313c3b577b | |
|
|
d2e1866582 | |
|
|
277bcd2759 | |
|
|
3223dcad66 | |
|
|
cb93fded6c | |
|
|
2ef0ad78e1 | |
|
|
a88446478e | |
|
|
fdcf280630 | |
|
|
617d809569 | |
|
|
fa317e6518 | |
|
|
97cf0b346e | |
|
|
beb34935a2 | |
|
|
268687f6c3 | |
|
|
12f5742c59 | |
|
|
dac52c2b8b | |
|
|
7c742dc880 | |
|
|
d66136e7bf | |
|
|
69fe1fd412 | |
|
|
a943e4f946 | |
|
|
5924cad2db | |
|
|
e12e658b56 | |
|
|
b8f03fd2ac | |
|
|
9be8790052 | |
|
|
fb8efe91aa | |
|
|
4437d60cf8 | |
|
|
697282f6ef | |
|
|
5b0c720c4f | |
|
|
a97a132320 | |
|
|
2be239d1fe | |
|
|
96ea6c0a92 | |
|
|
417b291f1f | |
|
|
f66af6c706 | |
|
|
8b83889659 | |
|
|
aba076cf36 | |
|
|
1838d3c26f | |
|
|
37a7901f5d | |
|
|
2fef514437 | |
|
|
437a32cb2b | |
|
|
d66b71d0a1 | |
|
|
63516582cc | |
|
|
cc30bd0a08 | |
|
|
50af09ba70 | |
|
|
11bcc10137 | |
|
|
dfc54b7b00 | |
|
|
58bfbf0703 | |
|
|
c22fe03bf3 | |
|
|
caae675135 | |
|
|
bc0eff5c55 |
|
|
@ -0,0 +1,8 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
# Dependency directory
|
# Dependency directory
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Build directories
|
||||||
|
src/public/dist/local/bundle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# example for dev
|
||||||
|
# example-project
|
||||||
|
|
||||||
# Remove some common IDE working directories
|
# Remove some common IDE working directories
|
||||||
.idea
|
.idea
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|
||||||
|
yarn.lock
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
.idea
|
||||||
|
example-project
|
||||||
|
docs
|
||||||
|
src/public/js
|
||||||
|
build
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM node:14-slim
|
||||||
|
|
||||||
|
WORKDIR /usr/src/codecrumbs
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 2018 3018
|
||||||
42
LICENSE
|
|
@ -1,21 +1,29 @@
|
||||||
MIT License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2018 Bohdan Liashenko
|
Copyright (c) 2019, Bohdan Liashenko
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Redistribution and use in source and binary forms, with or without
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
modification, are permitted provided that the following conditions are met:
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
copies or substantial portions of the Software.
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
this list of conditions and the following disclaimer in the documentation
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
and/or other materials provided with the distribution.
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
* Neither the name of the copyright holder nor the names of its
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
contributors may be used to endorse or promote products derived from
|
||||||
SOFTWARE.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
|
||||||
171
README.md
|
|
@ -1,2 +1,169 @@
|
||||||
# codecrumbs
|
[](https://badge.fury.io/js/codecrumbs) [](https://github.com/ellerbrock/open-source-badges/) [<img src="https://img.shields.io/twitter/follow/bliashenko.svg?label=Stay%20Tuned&style=social">](https://twitter.com/bliashenko)
|
||||||
Leave "breadcrumbs" in source code via comments to find your way out from code maze
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="/docs/logo-sm.png" width="250"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
<a href="#what">What</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="#demo">Demo</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="#get-started">Get started</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="#features">Features</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="#case-studies">Case studies</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="#support">Support</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
**Have you ever got lost in a big or unknown codebase?** This tool will help you to solve that. Also, it will increase your development speed and give more knowledge about your application architecture.
|
||||||
|
> If you like this project, follow me on Twitter [@bliashenko](https://twitter.com/bliashenko) to hear about things I am building.
|
||||||
|
|
||||||
|
## Codecrumbs v2
|
||||||
|
Check out new version of this project as [standalone application](https://codecrumbs.io). Just in a few clicks you can start exploring a codebase in more efficient way, create interactive visual guides and share them with others on your own blog! See [quick guide here](https://codecrumbs.io/guides/web-app-with-github/).
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://codecrumbs.io" target="_blank">
|
||||||
|
<img src="https://codecrumbs.io/external/img/common/app-ui-1.png" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
Check out prepared example for [**standalone version running here**](https://codecrumbs.io/app).
|
||||||
|
|
||||||
|
## Codecrumbs v1
|
||||||
|
|
||||||
|
>**How it works?** You run `codecrumbs` command for a codebase, it analyzes source code and builds its visual representation. Write down a codecrumb-comment and codebase state will be reflected by visual client in browser on the fly.
|
||||||
|
>
|
||||||
|
> Check out [my talk at React-Finland](https://www.youtube.com/watch?v=S_1-1jzLxm4) for more details.
|
||||||
|
|
||||||
|
|
||||||
|
<img src="/docs/main-ui-3.png" width="100%"/>
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
### Install and run
|
||||||
|
>Pre-condition: update/install `NodeJS` version to be >= *8.11.1*
|
||||||
|
|
||||||
|
1) Install ```codecrumbs``` globally (```yarn global add codecrumbs```)
|
||||||
|
2) Run ```codecrumbs -d project-src-dir -e project-src-dir/index.js```. Change parameters to match your project:```-d``` is *directory with source code*, ```-e``` is *entry point file* .
|
||||||
|
3) Go to [http://localhost:2018](http://localhost:2018/#) in the browser to check it out.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Run codecrumbs with CLI params or specify static config file `codecrumbs.config.js` (see example [here](/example-project/codecrumbs.config.js))
|
||||||
|
|
||||||
|
CLI | Config file | Description | Example
|
||||||
|
--- | --- | --- | ---
|
||||||
|
```d``` | ```projectDir``` | Relative path to project source code directory | ```-d src```
|
||||||
|
```e``` | ```entryPoint``` | Relative path to project source entry point file (must be inside ```dir```) | ```-e src/app.js```
|
||||||
|
```x``` | ```excludeDir``` | Relative path(or paths separated by ```,```) to directories for exclusion | ```-x src/doc,src/thirdparty```
|
||||||
|
```p``` | ```clientPort``` | Port for Codecrumbs client (optional, default *2018*) | ```-p 2019```
|
||||||
|
```n``` | ```projectNameAlias``` | Project name alias (optional, default same as ```-d``` value) | ```-n my-hello-world```
|
||||||
|
```C``` | - | Path to codecrumbs.config.js (optional, by default will try to find the file in PWD) | ```-C config/codecrumbs.config.js```
|
||||||
|
```D``` | ```debugModeEnabled``` | Enable debug mode for logs (optional, default is ```false```) | ```-D```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
### Breadcrumbs and trails
|
||||||
|
|
||||||
|
<img src="/docs/cc-ui-3.png" width="750"/>
|
||||||
|
|
||||||
|
UI explained:
|
||||||
|
- enable "Codecrumbs" switch to have codecrumbs tree on the scheme (drop-down contains extra configuration)
|
||||||
|
- choose "current" codecrumbs trail to display (can be either trail or all other "simple" codecrumbs)
|
||||||
|
- select connection between two steps (code for two codecrumbs will be opened in "Sidebar" under "Crumbs" tab)
|
||||||
|
- set other options in dropdowns to configure behaviour of the diagram (show code blocks, details, etc.)
|
||||||
|
|
||||||
|
**How to get there?**
|
||||||
|
|
||||||
|
Leave breadcrumb in code by writing down a comment: ```//cc:[parameters;]```.
|
||||||
|
|
||||||
|
```cc``` (stands for "CodeCrumb") is a prefix which used by the parser; check example of parameters in the table below:
|
||||||
|
|
||||||
|
Example | Description | Use case
|
||||||
|
--- | --- | ---
|
||||||
|
```//cc:remember place``` | simple breadcrumb, ```remember place``` is a title of our first breadcrumb | Mark an important place to not forget where it was
|
||||||
|
```//cc:here is bug;well, seems like a bug in logic``` | simple breadcrumb, ```well, seems like a bug in logic``` is details for breadcrumb, separated by ```;``` | Add extra information, will be rendered in popups
|
||||||
|
```//cc:signin#3;enable route``` | trail of breadcrumbs,```signin``` is the **trail ID**, ```#3``` is order **number of step**, ```enable route``` is a title describing the step. | A sequence of codecrumbs, use to describe some data flow (e.g. user login, or form submit, etc.).
|
||||||
|
```//cc:signin#1;firebase sign in;+2;do call to firebase with credentials``` | trail of breadcrumbs,```+2``` is number of lines to highlight, separated by ```;``` | Use number of lines to highlight the code related to breadcrumb
|
||||||
|
|
||||||
|
> Note: current version supports single line comments only.
|
||||||
|
|
||||||
|
> Hint: you can use trail id without step number (e.g. ```//cc:groupname#;test```) just to group breadcrumbs, you always can add step numbers later when you know the correct order.
|
||||||
|
|
||||||
|
### Multi-codebase integration
|
||||||
|
You might be interested to study connections between several codebases (sub-modules), codecrumbs supports that.
|
||||||
|
Simply start codecrumbs multiple times (once for each codebase), it all **will be synced in one picture** inside the browser tab. To control a diagram UI - select it by clicking on it.
|
||||||
|
|
||||||
|
E.g. for client-server application, go to the source directory for your server code and run `codecrumbs -e your-server-src/index.py -d your-server-src`, same for client `codecrumbs -e src-client/index.js -d src-client`.
|
||||||
|
> **Note:** codebases can be located wherever you want (**no** need to have them like mono-repo, etc.), simply run `codecrumbs` for directory you need.
|
||||||
|
|
||||||
|
<img src="/docs/multi-codebase-cc-2.png" width="100%"/>
|
||||||
|
|
||||||
|
### Multi-language support
|
||||||
|
Current version supports next programming languages:
|
||||||
|
- `C#`
|
||||||
|
- `C++`
|
||||||
|
- `Fortran`
|
||||||
|
- `Go`
|
||||||
|
- `Haskell`
|
||||||
|
- `Java`
|
||||||
|
- `JavaScript`
|
||||||
|
- `Kotlin`
|
||||||
|
- `PHP`
|
||||||
|
- `Python`
|
||||||
|
- `Ruby`
|
||||||
|
- `TypeScript`
|
||||||
|
|
||||||
|
Please file an issue to support other language you would like to have.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
> Note: In current version only [JavaScript, TypeScript] offer this feature
|
||||||
|
|
||||||
|
<img src="/docs/dep-ui-2.png" width="100%"/>
|
||||||
|
|
||||||
|
UI explained:
|
||||||
|
|
||||||
|
- enable "Dependencies" switch
|
||||||
|
- select connection between modules (all involved files will be opened in "Sidebar", so you can see “what is imported” and “its implementation”)
|
||||||
|
|
||||||
|
### Flowchart
|
||||||
|
> Note: In current version only JavaScript offers this feature
|
||||||
|
|
||||||
|
<img src="/docs/flow-ui.png" width="100%"/>
|
||||||
|
|
||||||
|
[js2flowchart](https://github.com/Bogdan-Lyashenko/js-code-to-svg-flowchart) is used in the sidebar to draw flowchart for the selected file code.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
Any support is very much appreciated! 👍 😘 ❤️
|
||||||
|
If you like this project, please, **put a :star: and tweet about it**. Thanks!
|
||||||
|
|
||||||
|
Please, consider [making financial donation](https://opencollective.com/codecrumbs), it will help further development of more cool features! We'll thank you by including your name/company logo here ☺️. Feel free to [ping me](https://www.linkedin.com/in/bohdan-liashenko-bb365854/) for discussion.
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/codecrumbs/donate" target="_blank">
|
||||||
|
<img src="https://opencollective.com/codecrumbs/donate/button@2x.png?color=blue" width=300 />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### Sponsors
|
||||||
|
Development supported by [0+X](https://0x.se)
|
||||||
|
|
||||||
|
<a href="https://0x.se" target="_blank">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/16350669?s=200&v=4" width=100 />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### Backers
|
||||||
|
<a href="https://opencollective.com/codecrumbs/backer/0/website" target="_blank"><img src="https://opencollective.com/codecrumbs/backer/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/codecrumbs/backer/1/website" target="_blank"><img src="https://opencollective.com/codecrumbs/backer/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/codecrumbs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/codecrumbs/sponsor/0/avatar.svg"></a>
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the [owner](https://github.com/Bogdan-Lyashenko/) of this repository before making a change. Ideas and suggestions are welcome.
|
||||||
|
To start development environment, clone the repo & run:
|
||||||
|
```javascript
|
||||||
|
yarn && yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
## WIP
|
||||||
|
Next features are developing:
|
||||||
|
- **VS Code extension** - some neat features right inside the code editor. Checkout [the repo here](https://github.com/Bogdan-Lyashenko/vs-code-codecrumbs).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
theme: jekyll-theme-cayman
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const program = require('commander');
|
||||||
|
const colors = require('colors');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const showUpdatesInfo = require('./updatesInfo');
|
||||||
|
const server = require('../src/server');
|
||||||
|
|
||||||
|
showUpdatesInfo();
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-e, --entry [entryPoint]', 'Specify path to entry point file. E.g. `src/app.js`')
|
||||||
|
.option('-d, --dir [projectDir]', 'Specify path to project source code directory. E.g. `src`', '')
|
||||||
|
.option(
|
||||||
|
'-w, --webpack [webpackConfigFile]',
|
||||||
|
'Specify path to webpack config file. E.g. webpack.config.js'
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
'-t, --tsconfig [tsConfigFile]',
|
||||||
|
'Specify path to typeScript config file. E.g. tsConfig.json'
|
||||||
|
)
|
||||||
|
.option('-p, --port [defaultPort]', 'Specify port for Codecrumbs client. E.g. 3333', 2018)
|
||||||
|
.option('-i, --ideCmd [ideCmd]', 'IDE command to open file')
|
||||||
|
.option('-x, --excludeDir [excludeDirectories]', 'Exclude directories')
|
||||||
|
.option('-n, --projectName [projectNameAlias]', 'Project name alias')
|
||||||
|
.option('-C, --configFile [pathToConfigFile]', 'Path to codecrumbs.config.js')
|
||||||
|
.option('-D, --debugModeEnabled [debugModeEnabled]', 'Enable debug mode for logs.')
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
const pathToConfigFile = program.configFile || 'codecrumbs.config.js';
|
||||||
|
const configFileExists = server.checkIfPathExists(pathToConfigFile);
|
||||||
|
if ((!program.entry || !program.dir) && !configFileExists) {
|
||||||
|
console.log(
|
||||||
|
colors.magenta(
|
||||||
|
'Please specify `entryPoint` and `projectDir` params (e.g. `codecrumbs -e src/app.js -d src`). Or use `-C codecrumbs.config.js` instead.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const configFromFile = configFileExists ? require(path.resolve(pathToConfigFile)) : {};
|
||||||
|
|
||||||
|
const configFromCLI = {
|
||||||
|
projectNameAlias: program.projectName,
|
||||||
|
entryPoint: program.entry,
|
||||||
|
projectDir: program.dir,
|
||||||
|
webpackConfigPath: program.webpack,
|
||||||
|
tsConfigPath: program.tsconfig,
|
||||||
|
clientPort: program.port,
|
||||||
|
excludeDir: program.excludeDir,
|
||||||
|
ideCmd: program.ideCmd,
|
||||||
|
debugModeEnabled: program.debugModeEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
server.setup(_.merge(configFromCLI, configFromFile), { isDev: false });
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const colors = require('colors');
|
||||||
|
const exec = require('child_process').exec;
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
try {
|
||||||
|
exec('npm outdated codecrumbs').stdout.on('data', function(data) {
|
||||||
|
const list = data
|
||||||
|
.split(' ')
|
||||||
|
.filter(v => !!v)
|
||||||
|
.map(v => v.trim());
|
||||||
|
|
||||||
|
const latestVersion = list[list.length - 2];
|
||||||
|
console.log(
|
||||||
|
colors.cyan.underline(
|
||||||
|
`There is new version of codecrumbs (${latestVersion}) available! Please update to have all latest features and improvements!`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 7.0 MiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
entryPoint: 'example-project/src-client/index.js',
|
||||||
|
projectDir: 'example-project/src-client',
|
||||||
|
clientPort: 1234,
|
||||||
|
projectNameAlias: 'example-project-for-client',
|
||||||
|
debugModeEnabled: true
|
||||||
|
};
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import move from '../zoom/move';
|
|
||||||
|
|
||||||
export default () => Promise.resolve({ data: [] });
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
//cc:debug#0;step 0
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
//cc:debug#2;step 2
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
//cc:debug#4;step 4
|
||||||
|
|
||||||
|
//cc:debug#1;step 1
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
//cc:debug#3;step 3
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import tabs from './views/tabs';
|
|
||||||
import dataModel from './dataModel/model';
|
|
||||||
|
|
||||||
const App = {
|
|
||||||
init() {
|
|
||||||
tabs.render(); //cc:render;CallLong line check out tabs.js for more details
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
App.init();
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
#include <iostream>
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
//cc:main function
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
cout << "Hello, World!";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
//cc:main function
|
||||||
|
using System;
|
||||||
|
namespace HelloWorld
|
||||||
|
{
|
||||||
|
class Hello
|
||||||
|
{
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Hello World!");
|
||||||
|
|
||||||
|
// Keep the console window open in debug mode.
|
||||||
|
Console.WriteLine("Press any key to exit.");
|
||||||
|
Console.ReadKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
! cc: main function
|
||||||
|
program hello
|
||||||
|
print *, "Hello World!"
|
||||||
|
end program hello
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// cc:main function
|
||||||
|
func main() {
|
||||||
|
fmt.Println("hello world")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- cc:main function
|
||||||
|
putStrLn "Hello, world!"
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
public class HelloWorld {
|
||||||
|
|
||||||
|
//cc: main function
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Prints "Hello, World" to the terminal window.
|
||||||
|
System.out.println("Hello, World");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
//cc:main function
|
||||||
|
function greeting() {
|
||||||
|
console.log('Hello world!');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
//cc:main function
|
||||||
|
|
||||||
|
fun main(args : Array<String>) {
|
||||||
|
println("Hello, World!")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- hello world program
|
||||||
|
print ("Hello World!")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* cc:main function */
|
||||||
|
let hello = () => "Hello, World!";
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
# cc: main function.
|
||||||
|
print "Hello, World!\n";
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
//cc:main function
|
||||||
|
echo "Hello World!";
|
||||||
|
echo "PHP is so easy!";
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# cc:main function
|
||||||
|
def main(argv=None):
|
||||||
|
if argv is None:
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
print "Hello, world"
|
||||||
|
|
||||||
|
return
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# cc:main function
|
||||||
|
|
||||||
|
puts 'Hello, world!'
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
//cc:main function
|
||||||
|
function greeting() {
|
||||||
|
console.log('Hello world!');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const INIT_AUTH = 'INIT_AUTH';
|
||||||
|
|
||||||
|
export const SIGN_IN_ERROR = 'SIGN_IN_ERROR';
|
||||||
|
export const SIGN_IN_SUCCESS = 'SIGN_IN_SUCCESS';
|
||||||
|
|
||||||
|
export const SIGN_OUT_SUCCESS = 'SIGN_OUT_SUCCESS';
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import firebase from 'firebase';
|
||||||
|
import { firebaseAuth } from '../firebase';
|
||||||
|
import {
|
||||||
|
INIT_AUTH,
|
||||||
|
SIGN_IN_ERROR,
|
||||||
|
SIGN_IN_SUCCESS,
|
||||||
|
SIGN_OUT_SUCCESS
|
||||||
|
} from './action-types';
|
||||||
|
|
||||||
|
|
||||||
|
function authenticate(provider) {
|
||||||
|
return dispatch => {
|
||||||
|
//cc:signin#1;firebase sign in;+1;call to firebase with auth provider, proceed if success response
|
||||||
|
firebaseAuth.signInWithPopup(provider)
|
||||||
|
.then(result => dispatch(signInSuccess(result)))
|
||||||
|
.catch(error => dispatch(signInError(error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function initAuth(user) {
|
||||||
|
return {
|
||||||
|
type: INIT_AUTH,
|
||||||
|
payload: user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signInError(error) {
|
||||||
|
return {
|
||||||
|
type: SIGN_IN_ERROR,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signInSuccess(result) {
|
||||||
|
return {
|
||||||
|
type: SIGN_IN_SUCCESS,
|
||||||
|
payload: result.user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signInWithGithub() {
|
||||||
|
return authenticate(new firebase.auth.GithubAuthProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signInWithGoogle() {
|
||||||
|
return authenticate(new firebase.auth.GoogleAuthProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signInWithTwitter() {
|
||||||
|
return authenticate(new firebase.auth.TwitterAuthProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signOut() {
|
||||||
|
return dispatch => {
|
||||||
|
firebaseAuth.signOut()
|
||||||
|
.then(() => dispatch(signOutSuccess()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function signOutSuccess() {
|
||||||
|
return {
|
||||||
|
type: SIGN_OUT_SUCCESS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { firebaseAuth } from '../firebase';
|
||||||
|
import * as authActions from './actions';
|
||||||
|
|
||||||
|
|
||||||
|
export function initAuth(dispatch) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const unsubscribe = firebaseAuth.onAuthStateChanged(
|
||||||
|
authUser => {
|
||||||
|
dispatch(authActions.initAuth(authUser));
|
||||||
|
unsubscribe();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
error => reject(error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as authActions from './actions';
|
||||||
|
|
||||||
|
|
||||||
|
export { authActions };
|
||||||
|
export * from './action-types';
|
||||||
|
export { initAuth } from './auth';
|
||||||
|
export { authReducer } from './reducer';
|
||||||
|
export { getAuth, isAuthenticated } from './selectors';
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
import { INIT_AUTH, SIGN_IN_SUCCESS, SIGN_OUT_SUCCESS } from './action-types';
|
||||||
|
|
||||||
|
|
||||||
|
export const AuthState = new Record({
|
||||||
|
authenticated: false,
|
||||||
|
id: null
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export function authReducer(state = new AuthState(), {payload, type}) {
|
||||||
|
switch (type) {
|
||||||
|
case INIT_AUTH:
|
||||||
|
case SIGN_IN_SUCCESS:
|
||||||
|
return state.merge({
|
||||||
|
authenticated: !!payload, //cc:signin#5;toggle 'authenticated' flag
|
||||||
|
id: payload ? payload.uid : null
|
||||||
|
});
|
||||||
|
|
||||||
|
case SIGN_OUT_SUCCESS:
|
||||||
|
return new AuthState();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
|
||||||
|
export function isAuthenticated(state) {
|
||||||
|
return getAuth(state).authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=====================================
|
||||||
|
// MEMOIZED SELECTORS
|
||||||
|
//-------------------------------------
|
||||||
|
|
||||||
|
export const getAuth = createSelector(
|
||||||
|
state => state.auth,
|
||||||
|
auth => auth.toJS()
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
//cc:firebase config;and some details
|
||||||
|
export const firebaseConfig = {
|
||||||
|
apiKey: 'AIzaSyBsVVpEDrlNPEmshLcmOuE0FxhjPn0AqMg',
|
||||||
|
authDomain: 'todo-react-redux.firebaseapp.com',
|
||||||
|
databaseURL: 'https://todo-react-redux.firebaseio.com',
|
||||||
|
storageBucket: 'firebase-todo-react-redux.appspot.com'
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { firebaseDb } from './firebase';
|
||||||
|
|
||||||
|
|
||||||
|
export class FirebaseList {
|
||||||
|
constructor(actions, modelClass, path = null) {
|
||||||
|
this._actions = actions;
|
||||||
|
this._modelClass = modelClass;
|
||||||
|
this._path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
return this._path;
|
||||||
|
}
|
||||||
|
|
||||||
|
set path(value) {
|
||||||
|
this._path = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(value) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
firebaseDb.ref(this._path)
|
||||||
|
.push(value, error => error ? reject(error) : resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
firebaseDb.ref(`${this._path}/${key}`)
|
||||||
|
.remove(error => error ? reject(error) : resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
firebaseDb.ref(`${this._path}/${key}`)
|
||||||
|
.set(value, error => error ? reject(error) : resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(key, value) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
firebaseDb.ref(`${this._path}/${key}`)
|
||||||
|
.update(value, error => error ? reject(error) : resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(emit) {
|
||||||
|
let ref = firebaseDb.ref(this._path);
|
||||||
|
let initialized = false;
|
||||||
|
let list = [];
|
||||||
|
|
||||||
|
ref.once('value', () => {
|
||||||
|
initialized = true;
|
||||||
|
emit(this._actions.onLoad(list));
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.on('child_added', snapshot => {
|
||||||
|
if (initialized) {
|
||||||
|
emit(this._actions.onAdd(this.unwrapSnapshot(snapshot)));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
list.push(this.unwrapSnapshot(snapshot));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.on('child_changed', snapshot => {
|
||||||
|
emit(this._actions.onChange(this.unwrapSnapshot(snapshot)));
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.on('child_removed', snapshot => {
|
||||||
|
emit(this._actions.onRemove(this.unwrapSnapshot(snapshot)));
|
||||||
|
});
|
||||||
|
|
||||||
|
this._unsubscribe = () => ref.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe() {
|
||||||
|
this._unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrapSnapshot(snapshot) {
|
||||||
|
let attrs = snapshot.val();
|
||||||
|
attrs.key = snapshot.key;
|
||||||
|
return new this._modelClass(attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import firebase from 'firebase/app';
|
||||||
|
|
||||||
|
import 'firebase/auth';
|
||||||
|
import 'firebase/database';
|
||||||
|
|
||||||
|
import { firebaseConfig } from './config';
|
||||||
|
|
||||||
|
export const firebaseApp = firebase.initializeApp(firebaseConfig);
|
||||||
|
export const firebaseAuth = firebase.auth();
|
||||||
|
export const firebaseDb = firebase.database();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { firebaseApp, firebaseAuth, firebaseDb } from './firebase';
|
||||||
|
export { FirebaseList } from './firebase-list';
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import createHistory from 'history/createBrowserHistory';
|
||||||
|
|
||||||
|
|
||||||
|
export default createHistory();
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import './views/styles/styles.css';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { ConnectedRouter } from 'react-router-redux';
|
||||||
|
|
||||||
|
import { initAuth } from './auth';
|
||||||
|
import history from './history';
|
||||||
|
import configureStore from './store';
|
||||||
|
import registerServiceWorker from './utils/register-service-worker';
|
||||||
|
import App from './views/app';
|
||||||
|
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
|
||||||
|
//cc:layout#0;start
|
||||||
|
function render(Component) {
|
||||||
|
ReactDOM.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<div>
|
||||||
|
<Component/>
|
||||||
|
</div>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>,
|
||||||
|
rootElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept('./views/app', () => {
|
||||||
|
render(require('./views/app').default);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
registerServiceWorker();
|
||||||
|
|
||||||
|
initAuth(store.dispatch)
|
||||||
|
.then(() => render(App))
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION';
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { DISMISS_NOTIFICATION } from './action-types';
|
||||||
|
|
||||||
|
|
||||||
|
export function dismissNotification() {
|
||||||
|
return {
|
||||||
|
type: DISMISS_NOTIFICATION
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as notificationActions from './actions';
|
||||||
|
|
||||||
|
|
||||||
|
export { notificationActions };
|
||||||
|
export * from './action-types';
|
||||||
|
export { notificationReducer } from './reducer';
|
||||||
|
export { getNotification } from './selectors';
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
import { REMOVE_TASK_SUCCESS } from '../tasks';
|
||||||
|
import { DISMISS_NOTIFICATION } from './action-types';
|
||||||
|
|
||||||
|
|
||||||
|
export const NotificationState = new Record({
|
||||||
|
actionLabel: '',
|
||||||
|
display: false,
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export function notificationReducer(state = new NotificationState(), action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case REMOVE_TASK_SUCCESS:
|
||||||
|
return state.merge({
|
||||||
|
actionLabel: 'Undo',
|
||||||
|
display: true,
|
||||||
|
message: 'Task deleted'
|
||||||
|
});
|
||||||
|
|
||||||
|
case DISMISS_NOTIFICATION:
|
||||||
|
return new NotificationState();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new NotificationState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function getNotification(state) {
|
||||||
|
return state.notification;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { routerReducer } from 'react-router-redux';
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { authReducer } from './auth';
|
||||||
|
import { notificationReducer } from './notification';
|
||||||
|
import { tasksReducer } from './tasks';
|
||||||
|
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
auth: authReducer,
|
||||||
|
notification: notificationReducer,
|
||||||
|
routing: routerReducer,
|
||||||
|
tasks: tasksReducer
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { routerMiddleware } from 'react-router-redux';
|
||||||
|
import { applyMiddleware, compose, createStore } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import history from './history';
|
||||||
|
import reducers from './reducers';
|
||||||
|
|
||||||
|
|
||||||
|
export default (initialState = {}) => {
|
||||||
|
let middleware = applyMiddleware(thunk, routerMiddleware(history));
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
const devToolsExtension = window.devToolsExtension;
|
||||||
|
if (typeof devToolsExtension === 'function') {
|
||||||
|
middleware = compose(middleware, devToolsExtension());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = createStore(reducers, initialState, middleware);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept('./reducers', () => {
|
||||||
|
store.replaceReducer(require('./reducers').default);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const CREATE_TASK_ERROR = 'CREATE_TASK_ERROR';
|
||||||
|
export const CREATE_TASK_SUCCESS = 'CREATE_TASK_SUCCESS';
|
||||||
|
|
||||||
|
export const REMOVE_TASK_ERROR = 'REMOVE_TASK_ERROR';
|
||||||
|
export const REMOVE_TASK_SUCCESS = 'REMOVE_TASK_SUCCESS';
|
||||||
|
|
||||||
|
export const UNDELETE_TASK_ERROR = 'UNDELETE_TASK_ERROR';
|
||||||
|
|
||||||
|
export const UPDATE_TASK_ERROR = 'UPDATE_TASK_ERROR';
|
||||||
|
export const UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS';
|
||||||
|
|
||||||
|
export const FILTER_TASKS = 'FILTER_TASKS';
|
||||||
|
export const LOAD_TASKS_SUCCESS = 'LOAD_TASKS_SUCCESS';
|
||||||
|
export const UNLOAD_TASKS_SUCCESS = 'UNLOAD_TASKS_SUCCESS';
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { getDeletedTask } from './selectors';
|
||||||
|
import { taskList } from './task-list';
|
||||||
|
import {
|
||||||
|
CREATE_TASK_ERROR,
|
||||||
|
CREATE_TASK_SUCCESS,
|
||||||
|
REMOVE_TASK_ERROR,
|
||||||
|
REMOVE_TASK_SUCCESS,
|
||||||
|
FILTER_TASKS,
|
||||||
|
LOAD_TASKS_SUCCESS,
|
||||||
|
UNDELETE_TASK_ERROR,
|
||||||
|
UNLOAD_TASKS_SUCCESS,
|
||||||
|
UPDATE_TASK_ERROR,
|
||||||
|
UPDATE_TASK_SUCCESS
|
||||||
|
} from './action-types';
|
||||||
|
|
||||||
|
|
||||||
|
export function createTask(title) {
|
||||||
|
return dispatch => {
|
||||||
|
taskList.push({completed: false, title})
|
||||||
|
.catch(error => dispatch(createTaskError(error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTaskError(error) {
|
||||||
|
return {
|
||||||
|
type: CREATE_TASK_ERROR,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTaskSuccess(task) {
|
||||||
|
return {
|
||||||
|
type: CREATE_TASK_SUCCESS,
|
||||||
|
payload: task
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTask(task) {
|
||||||
|
return dispatch => {
|
||||||
|
taskList.remove(task.key)
|
||||||
|
.catch(error => dispatch(removeTaskError(error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTaskError(error) {
|
||||||
|
return {
|
||||||
|
type: REMOVE_TASK_ERROR,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTaskSuccess(task) {
|
||||||
|
return {
|
||||||
|
type: REMOVE_TASK_SUCCESS,
|
||||||
|
payload: task
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function undeleteTask() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const task = getDeletedTask(getState());
|
||||||
|
if (task) {
|
||||||
|
taskList.set(task.key, {completed: task.completed, title: task.title})
|
||||||
|
.catch(error => dispatch(undeleteTaskError(error)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function undeleteTaskError(error) {
|
||||||
|
return {
|
||||||
|
type: UNDELETE_TASK_ERROR,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTaskError(error) {
|
||||||
|
return {
|
||||||
|
type: UPDATE_TASK_ERROR,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTask(task, changes) {
|
||||||
|
return dispatch => {
|
||||||
|
taskList.update(task.key, changes)
|
||||||
|
.catch(error => dispatch(updateTaskError(error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTaskSuccess(task) {
|
||||||
|
return {
|
||||||
|
type: UPDATE_TASK_SUCCESS,
|
||||||
|
payload: task
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTasksSuccess(tasks) {
|
||||||
|
return {
|
||||||
|
type: LOAD_TASKS_SUCCESS,
|
||||||
|
payload: tasks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterTasks(filterType) {
|
||||||
|
return {
|
||||||
|
type: FILTER_TASKS,
|
||||||
|
payload: {filterType}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTasks() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { auth } = getState();
|
||||||
|
taskList.path = `tasks/${auth.id}`;
|
||||||
|
taskList.subscribe(dispatch);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unloadTasks() {
|
||||||
|
taskList.unsubscribe();
|
||||||
|
return {
|
||||||
|
type: UNLOAD_TASKS_SUCCESS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as tasksActions from './actions';
|
||||||
|
|
||||||
|
|
||||||
|
export { tasksActions };
|
||||||
|
export * from './action-types';
|
||||||
|
export { tasksReducer } from './reducer';
|
||||||
|
export { getTaskFilter, getVisibleTasks } from './selectors';
|
||||||
|
export { Task } from './task';
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { List, Record } from 'immutable';
|
||||||
|
import { SIGN_OUT_SUCCESS } from '../auth/action-types';
|
||||||
|
import {
|
||||||
|
CREATE_TASK_SUCCESS,
|
||||||
|
REMOVE_TASK_SUCCESS,
|
||||||
|
FILTER_TASKS,
|
||||||
|
LOAD_TASKS_SUCCESS,
|
||||||
|
UPDATE_TASK_SUCCESS
|
||||||
|
} from './action-types';
|
||||||
|
|
||||||
|
|
||||||
|
export const TasksState = new Record({
|
||||||
|
deleted: null,
|
||||||
|
filter: '',
|
||||||
|
list: new List(),
|
||||||
|
previous: null
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export function tasksReducer(state = new TasksState(), {payload, type}) {
|
||||||
|
switch (type) {
|
||||||
|
case CREATE_TASK_SUCCESS:
|
||||||
|
return state.merge({
|
||||||
|
deleted: null,
|
||||||
|
previous: null,
|
||||||
|
list: state.deleted && state.deleted.key === payload.key ?
|
||||||
|
state.previous :
|
||||||
|
state.list.unshift(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
case REMOVE_TASK_SUCCESS:
|
||||||
|
return state.merge({
|
||||||
|
deleted: payload,
|
||||||
|
previous: state.list,
|
||||||
|
list: state.list.filter(task => task.key !== payload.key)
|
||||||
|
});
|
||||||
|
|
||||||
|
case FILTER_TASKS:
|
||||||
|
return state.set('filter', payload.filterType || '');
|
||||||
|
|
||||||
|
case LOAD_TASKS_SUCCESS:
|
||||||
|
return state.set('list', new List(payload.reverse()));
|
||||||
|
|
||||||
|
case UPDATE_TASK_SUCCESS:
|
||||||
|
return state.merge({
|
||||||
|
deleted: null,
|
||||||
|
previous: null,
|
||||||
|
list: state.list.map(task => {
|
||||||
|
return task.key === payload.key ? payload : task;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
case SIGN_OUT_SUCCESS:
|
||||||
|
return new TasksState();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
|
||||||
|
export function getTasks(state) {
|
||||||
|
return state.tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskList(state) {
|
||||||
|
return getTasks(state).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskFilter(state) {
|
||||||
|
return getTasks(state).filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeletedTask(state) {
|
||||||
|
return getTasks(state).deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=====================================
|
||||||
|
// MEMOIZED SELECTORS
|
||||||
|
//-------------------------------------
|
||||||
|
|
||||||
|
export const getVisibleTasks = createSelector(
|
||||||
|
getTaskList,
|
||||||
|
getTaskFilter,
|
||||||
|
(tasks, filter) => {
|
||||||
|
switch (filter) {
|
||||||
|
case 'active':
|
||||||
|
return tasks.filter(task => !task.completed);
|
||||||
|
|
||||||
|
case 'completed':
|
||||||
|
return tasks.filter(task => task.completed);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { FirebaseList } from '../firebase';
|
||||||
|
import * as taskActions from './actions';
|
||||||
|
import { Task } from './task';
|
||||||
|
|
||||||
|
|
||||||
|
export const taskList = new FirebaseList({
|
||||||
|
onAdd: taskActions.createTaskSuccess,
|
||||||
|
onChange: taskActions.updateTaskSuccess,
|
||||||
|
onLoad: taskActions.loadTasksSuccess,
|
||||||
|
onRemove: taskActions.removeTaskSuccess
|
||||||
|
}, Task);
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
|
||||||
|
export const Task = new Record({
|
||||||
|
completed: false,
|
||||||
|
key: null,
|
||||||
|
title: null
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p>{{ greeting }} World!</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
module.exports = {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
greeting: 'Hello'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
//cc:test vue
|
||||||
|
<style scoped>
|
||||||
|
p {
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { findRenderedComponentWithType, renderIntoDocument } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
|
||||||
|
export function createTestComponent(TestComponent, props) {
|
||||||
|
return findRenderedComponentWithType(
|
||||||
|
renderIntoDocument(<TestComponent {...props}/>),
|
||||||
|
TestComponent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
// In production, we register a service worker to serve assets from local cache.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||||
|
// cached resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||||
|
// This link also includes instructions on opting out of this behavior.
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function register() {
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||||
|
if (publicUrl.origin !== window.location.origin) return;
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
if (!isLocalhost) {
|
||||||
|
registerValidSW(swUrl);
|
||||||
|
} else {
|
||||||
|
checkValidServiceWorker(swUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the old content will have been purged and
|
||||||
|
// the fresh content will have been added to the cache.
|
||||||
|
// It's the perfect time to display a "New content is
|
||||||
|
// available; please refresh." message in your web app.
|
||||||
|
console.log('New content is available; please refresh.');
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl)
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
response.headers.get('content-type').indexOf('javascript') === -1
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { authActions, getAuth } from '../../auth';
|
||||||
|
import Header from '../components/header';
|
||||||
|
import RequireAuthRoute from '../components/require-auth-route';
|
||||||
|
import RequireUnauthRoute from '../components/require-unauth-route';
|
||||||
|
import SignInPage from '../pages/sign-in';
|
||||||
|
import TasksPage from '../pages/tasks';
|
||||||
|
|
||||||
|
//cc:layout#1;describe pages;some details long description for separare popup
|
||||||
|
const App = ({authenticated, signOut}) => (
|
||||||
|
<div>
|
||||||
|
<Header
|
||||||
|
authenticated={authenticated}
|
||||||
|
signOut={signOut}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<RequireAuthRoute authenticated={authenticated} exact path="/" component={TasksPage}/>
|
||||||
|
<RequireUnauthRoute authenticated={authenticated} path="/sign-in" component={SignInPage}/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
authenticated: PropTypes.bool.isRequired,
|
||||||
|
signOut: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//=====================================
|
||||||
|
// CONNECT
|
||||||
|
//-------------------------------------
|
||||||
|
|
||||||
|
const mapStateToProps = getAuth;
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
signOut: authActions.signOut
|
||||||
|
};
|
||||||
|
|
||||||
|
//cc:here
|
||||||
|
export default withRouter(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(App)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './app';
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './button.css';
|
||||||
|
|
||||||
|
|
||||||
|
const Button = ({children, className, onClick, type = 'button'}) => {
|
||||||
|
const cssClasses = classNames('btn', className);
|
||||||
|
return (
|
||||||
|
<button className={cssClasses} onClick={onClick} type={type}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
type: PropTypes.oneOf(['button', 'reset', 'submit'])
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
@import 'views/styles/shared';
|
||||||
|
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@include button-base;
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--icon {
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './button';
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
export default function GitHubLogo() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 20 20">
|
||||||
|
<path d="M10 0C4.5 0 0 4.5 0 10c0 4.4 2.9 8.2 6.8 9.5.5.1.7-.2.7-.5v-1.9c-2.5.5-3.2-.6-3.4-1.1-.1-.3-.6-1.2-1-1.4-.4-.2-.9-.6 0-.7.8 0 1.3.7 1.5 1 .9 1.5 2.4 1.1 3 .9.1-.6.4-1.1.6-1.3-2.2-.3-4.6-1.2-4.6-5 0-1.1.4-2 1-2.7 0-.3-.4-1.3.2-2.7 0 0 .8-.3 2.8 1 .7-.2 1.6-.3 2.4-.3s1.7.1 2.5.3c1.9-1.3 2.8-1 2.8-1 .5 1.4.2 2.4.1 2.7.6.7 1 1.6 1 2.7 0 3.8-2.3 4.7-4.6 4.9.4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5 4-1.3 6.8-5.1 6.8-9.5C20 4.5 15.5 0 10 0z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './github-logo';
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Button from '../button';
|
||||||
|
import GitHubLogo from '../github-logo';
|
||||||
|
|
||||||
|
import './header.css';
|
||||||
|
|
||||||
|
const Header = ({authenticated, signOut}) => (
|
||||||
|
<header className="header">
|
||||||
|
<div className="g-row">
|
||||||
|
<div className="g-col">
|
||||||
|
<h1 className="header__title">Todo React Redux</h1>
|
||||||
|
|
||||||
|
<ul className="header__actions">
|
||||||
|
{authenticated ? <li><Button onClick={signOut}>Sign out</Button></li> : null}
|
||||||
|
<li>
|
||||||
|
<a className="link link--github" href="https://github.com/r-park/todo-react-redux">
|
||||||
|
<GitHubLogo />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
authenticated: PropTypes.bool.isRequired,
|
||||||
|
signOut: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
@import 'views/styles/shared';
|
||||||
|
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 10px 0;
|
||||||
|
height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
float: left;
|
||||||
|
font-size: rem(14px);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 40px;
|
||||||
|
text-rendering: auto;
|
||||||
|
transform: translate(0,0);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: inline-block;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__actions {
|
||||||
|
@include clearfix;
|
||||||
|
float: right;
|
||||||
|
padding: 8px 0;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
float: left;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: rem(14px);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: block;
|
||||||
|
fill: #98999a;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link--github {
|
||||||
|
padding-top: 1px;
|
||||||
|
width: 22px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './header';
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
||||||
|
const Icon = ({className, name}) => {
|
||||||
|
const cssClasses = classNames('material-icons', className);
|
||||||
|
return <span className={cssClasses}>{name}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Icon.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default Icon;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './icon';
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './notification';
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './notification.css';
|
||||||
|
|
||||||
|
|
||||||
|
class Notification extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
action: PropTypes.func.isRequired,
|
||||||
|
actionLabel: PropTypes.string.isRequired,
|
||||||
|
dismiss: PropTypes.func.isRequired,
|
||||||
|
display: PropTypes.bool.isRequired,
|
||||||
|
duration: PropTypes.number,
|
||||||
|
message: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.display) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.clearTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimer() {
|
||||||
|
if (this.timerId) {
|
||||||
|
clearTimeout(this.timerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
this.clearTimer();
|
||||||
|
this.timerId = setTimeout(() => {
|
||||||
|
this.props.dismiss();
|
||||||
|
}, this.props.duration || 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="notification">
|
||||||
|
<p className="notification__message" ref={c => this.message = c}>{this.props.message}</p>
|
||||||
|
<button
|
||||||
|
className="btn notification__button"
|
||||||
|
onClick={this.props.action}
|
||||||
|
ref={c => this.button = c}
|
||||||
|
type="button">{this.props.actionLabel}</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification;
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
@import 'views/styles/shared';
|
||||||
|
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
@include clearfix;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 60px;
|
||||||
|
margin-left: -100px;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 10px 15px;
|
||||||
|
width: 200px;
|
||||||
|
font-size: rem(16px);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__message {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__button {
|
||||||
|
float: right;
|
||||||
|
font-size: rem(16px);
|
||||||
|
line-height: 24px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #85bf6b;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './require-auth-route';
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
|
//cc:signin#6;enable route;details
|
||||||
|
const RequireAuthRoute = ({component: Component, authenticated, ...rest}) => (
|
||||||
|
<Route
|
||||||
|
{...rest}
|
||||||
|
render={props => {
|
||||||
|
return authenticated ? (
|
||||||
|
<Component {...props}/>
|
||||||
|
) : (
|
||||||
|
<Redirect to={{
|
||||||
|
pathname: '/sign-in',
|
||||||
|
state: {from: props.location}
|
||||||
|
}}/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export default RequireAuthRoute;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './require-unauth-route';
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
|
|
||||||
|
const RequireUnauthRoute = ({component: Component, authenticated, ...rest}) => (
|
||||||
|
<Route
|
||||||
|
{...rest}
|
||||||
|
render={props => {
|
||||||
|
return authenticated ? (
|
||||||
|
<Redirect to={{
|
||||||
|
pathname: '/',
|
||||||
|
state: {from: props.location}
|
||||||
|
}}/>
|
||||||
|
) : (
|
||||||
|
<Component {...props}/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export default RequireUnauthRoute;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './task-filters';
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import './task-filters.css';
|
||||||
|
|
||||||
|
|
||||||
|
const TaskFilters = ({filter}) => (
|
||||||
|
<ul className="task-filters">
|
||||||
|
<li><NavLink isActive={() => !filter} to="/">View All</NavLink></li>
|
||||||
|
<li><NavLink isActive={() => filter === 'active'} to={{pathname: '/', search: '?filter=active'}}>Active</NavLink></li>
|
||||||
|
<li><NavLink isActive={() => filter === 'completed'} to={{pathname: '/', search: '?filter=completed'}}>Completed</NavLink></li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
TaskFilters.propTypes = {
|
||||||
|
filter: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default TaskFilters;
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
@import 'views/styles/shared';
|
||||||
|
|
||||||
|
|
||||||
|
.task-filters {
|
||||||
|
@include clearfix;
|
||||||
|
margin-bottom: 45px;
|
||||||
|
padding-left: 1px;
|
||||||
|
font-size: rem(16px);
|
||||||
|
line-height: 24px;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
@include media-query(540) {
|
||||||
|
margin-bottom: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:first-child):before {
|
||||||
|
padding-right: 12px;
|
||||||
|
content: '/';
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #999;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './task-form';
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './task-form.css';
|
||||||
|
|
||||||
|
|
||||||
|
export class TaskForm extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
handleSubmit: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.state = {title: ''};
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleKeyUp = this.handleKeyUp.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInput() {
|
||||||
|
this.setState({title: ''});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event) {
|
||||||
|
this.setState({title: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyUp(event) {
|
||||||
|
if (event.keyCode === 27) this.clearInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const title = this.state.title.trim();
|
||||||
|
if (title.length) this.props.handleSubmit(title);
|
||||||
|
this.clearInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<form className="task-form" onSubmit={this.handleSubmit} noValidate>
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
className="task-form__input"
|
||||||
|
maxLength="64"
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onKeyUp={this.handleKeyUp}
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
ref={e => this.titleInput = e}
|
||||||
|
type="text"
|
||||||
|
value={this.state.title}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default TaskForm;
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
@import 'views/styles/shared';
|
||||||
|
|
||||||
|
|
||||||
|
.task-form {
|
||||||
|
margin: 40px 0 10px;
|
||||||
|
|
||||||
|
@include media-query(540) {
|
||||||
|
margin: 80px 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form__input {
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px dotted #666;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 0 5px 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: rem(24px);
|
||||||
|
font-weight: 300;
|
||||||
|
color: #fff;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
@include media-query(540) {
|
||||||
|
height: 61px;
|
||||||
|
font-size: rem(32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #999;
|
||||||
|
opacity: 1; // firefox native placeholder style has opacity < 1
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::placeholder {
|
||||||
|
color: #777;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// webkit input doesn't inherit font-smoothing from ancestors
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
|
// remove `x`
|
||||||
|
&::-ms-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './task-item';
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Button from '../button';
|
||||||
|
import Icon from '../icon';
|
||||||
|
|
||||||
|
import './task-item.css';
|
||||||
|
|
||||||
|
//cc:there is task;extra
|
||||||
|
export class TaskItem extends Component {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.state = {editing: false};
|
||||||
|
|
||||||
|
this.edit = this.edit.bind(this);
|
||||||
|
this.handleKeyUp = this.handleKeyUp.bind(this);
|
||||||
|
this.remove = this.remove.bind(this);
|
||||||
|
this.save = this.save.bind(this);
|
||||||
|
this.stopEditing = this.stopEditing.bind(this);
|
||||||
|
this.toggleStatus = this.toggleStatus.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
this.setState({editing: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyUp(event) {
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
this.save(event);
|
||||||
|
}
|
||||||
|
else if (event.keyCode === 27) {
|
||||||
|
this.stopEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.props.removeTask(this.props.task);
|
||||||
|
}
|
||||||
|
|
||||||
|
save(event) {
|
||||||
|
if (this.state.editing) {
|
||||||
|
const { task } = this.props;
|
||||||
|
const title = event.target.value.trim();
|
||||||
|
|
||||||
|
if (title.length && title !== task.title) {
|
||||||
|
this.props.updateTask(task, {title});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEditing() {
|
||||||
|
this.setState({editing: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleStatus() {
|
||||||
|
const { task } = this.props;
|
||||||
|
this.props.updateTask(task, {completed: !task.completed});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitle(task) {
|
||||||
|
return (
|
||||||
|
<div className="task-item__title" tabIndex="0">
|
||||||
|
{task.title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitleInput(task) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
className="task-item__input"
|
||||||
|
defaultValue={task.title}
|
||||||
|
maxLength="64"
|
||||||
|
onKeyUp={this.handleKeyUp}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { editing } = this.state;
|
||||||
|
const { task } = this.props;
|
||||||
|
|
||||||
|
let containerClasses = classNames('task-item', {
|
||||||
|
'task-item--completed': task.completed,
|
||||||
|
'task-item--editing': editing
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClasses} tabIndex="0">
|
||||||
|
<div className="cell">
|
||||||
|
<Button
|
||||||
|
className={classNames('btn--icon', 'task-item__button', {'active': task.completed, 'hide': editing})}
|
||||||
|
onClick={this.toggleStatus}>
|
||||||
|
<Icon name="done" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cell">
|
||||||
|
{editing ? this.renderTitleInput(task) : this.renderTitle(task)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cell">
|
||||||
|
<Button
|
||||||
|
className={classNames('btn--icon', 'task-item__button', {'hide': editing})}
|
||||||
|
onClick={this.edit}>
|
||||||
|
<Icon name="mode_edit" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={classNames('btn--icon', 'task-item__button', {'hide': !editing})}
|
||||||
|
onClick={this.stopEditing}>
|
||||||
|
<Icon name="clear" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={classNames('btn--icon', 'task-item__button', {'hide': editing})}
|
||||||
|
onClick={this.remove}>
|
||||||
|
<Icon name="delete" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskItem.propTypes = {
|
||||||
|
removeTask: PropTypes.func.isRequired,
|
||||||
|
task: PropTypes.object.isRequired,
|
||||||
|
updateTask: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default TaskItem;
|
||||||