Compare commits
683 Commits
open-relea
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8116485b0 | ||
|
|
762ff75fc4 | ||
|
|
095b156b95 | ||
|
|
033e0fd7c5 | ||
|
|
1ecdb0b6af | ||
|
|
18951cc4d0 | ||
|
|
4dd5ddcc8b | ||
|
|
6acbf64a71 | ||
|
|
aea12a6a37 | ||
|
|
3d778807f1 | ||
|
|
014a990c22 | ||
|
|
7c8051a440 | ||
|
|
8f8531a242 | ||
|
|
319c48f1c8 | ||
|
|
fbd73bfbfe | ||
|
|
27a63cf406 | ||
|
|
7ea351f6a0 | ||
|
|
61e8c254d7 | ||
|
|
3a08e790c3 | ||
|
|
b4c5171886 | ||
|
|
7b83c416f8 | ||
|
|
a3c261bb13 | ||
|
|
98d03aa29f | ||
|
|
f5d5e2fd02 | ||
|
|
490bf27ed1 | ||
|
|
780acac2fd | ||
|
|
2ea763701d | ||
|
|
e2d9ba5857 | ||
|
|
747d656f0a | ||
|
|
8638ed5cf4 | ||
|
|
ca2e7f554a | ||
|
|
0e94124d74 | ||
|
|
af7edd8a3f | ||
|
|
9323f119c8 | ||
|
|
38a1924c6a | ||
|
|
2d7303009f | ||
|
|
dfcb94a831 | ||
|
|
520dd6ed6b | ||
|
|
1b32dbfa19 | ||
|
|
f7d9bdb5b5 | ||
|
|
063ec80cde | ||
|
|
3cbb134c3a | ||
|
|
059a60d1c8 | ||
|
|
c88d701271 | ||
|
|
33b98b356b | ||
|
|
1f81699af4 | ||
|
|
13aa77fc70 | ||
|
|
66531831b7 | ||
|
|
846d3f0662 | ||
|
|
f78e84ee0a | ||
|
|
2d27da2391 | ||
|
|
78413be34a | ||
|
|
88866a39c1 | ||
|
|
dc9699c033 | ||
|
|
00a0e27062 | ||
|
|
6839afcf3c | ||
|
|
1cd9c58c1a | ||
|
|
5d481a93c7 | ||
|
|
438d1fcfa7 | ||
|
|
bc912ce139 | ||
|
|
ab1c2d5379 | ||
|
|
c109f6e771 | ||
|
|
8976647190 | ||
|
|
cb051a83ad | ||
|
|
1b8aec5709 | ||
|
|
9a68e95fcc | ||
|
|
c90980afb0 | ||
|
|
abb8ae5085 | ||
|
|
8bb7462098 | ||
|
|
b4a5397ba1 | ||
|
|
a43c620dc4 | ||
|
|
93d11b8485 | ||
|
|
68e13d4daf | ||
|
|
f6617935e3 | ||
|
|
5f4591c046 | ||
|
|
ae52a8cb65 | ||
|
|
b8df66ad23 | ||
|
|
923776ab96 | ||
|
|
f170f5e3f0 | ||
|
|
730875ceb2 | ||
|
|
812350d24a | ||
|
|
6879bacb89 | ||
|
|
9b2b0f2019 | ||
|
|
87884f2d91 | ||
|
|
3e20fcae57 | ||
|
|
173896811d | ||
|
|
7af4a08bd9 | ||
|
|
6c12b3b034 | ||
|
|
5304085cd8 | ||
|
|
3cd9ae130c | ||
|
|
28ad2c2cf6 | ||
|
|
3e889df109 | ||
|
|
52c6efc34d | ||
|
|
584a84a99c | ||
|
|
7e4ab1c74c | ||
|
|
13d89cb3a0 | ||
|
|
5a1e2e6c97 | ||
|
|
6f1cf29a60 | ||
|
|
159f1ae30e | ||
|
|
e2e626552f | ||
|
|
308d7c62e4 | ||
|
|
0bc78da55d | ||
|
|
6527caea54 | ||
|
|
a52912e35b | ||
|
|
6479382b90 | ||
|
|
4ce36bb12c | ||
|
|
4cc7723984 | ||
|
|
3c3d359d4e | ||
|
|
cccbf3a9d1 | ||
|
|
4a3fd2ee8e | ||
|
|
48d7cb386a | ||
|
|
bdf9cab869 | ||
|
|
be02dabf40 | ||
|
|
c535fb9d24 | ||
|
|
8ab8d09b97 | ||
|
|
286c70d50f | ||
|
|
8939e5b91f | ||
|
|
bc9f7b3bce | ||
|
|
fd0bcb9e5f | ||
|
|
98e0167ef1 | ||
|
|
8091085f45 | ||
|
|
cd5abd1d9c | ||
|
|
2a88f435b9 | ||
|
|
fe1e9c5629 | ||
|
|
0e363ca724 | ||
|
|
c874638bd1 | ||
|
|
e5c3b1ed41 | ||
|
|
8a27b8cc37 | ||
|
|
046fbeab01 | ||
|
|
27ea509989 | ||
|
|
27f0508e6e | ||
|
|
c53fedf7a1 | ||
|
|
0f1a5e9aef | ||
|
|
6cb4b799b7 | ||
|
|
439b9161b5 | ||
|
|
6ffa45f0c1 | ||
|
|
a03ba3e3b3 | ||
|
|
e2a206caa5 | ||
|
|
3a963da819 | ||
|
|
4a65f0a84c | ||
|
|
9688bd3699 | ||
|
|
c123815a55 | ||
|
|
182e669593 | ||
|
|
65533b8d58 | ||
|
|
45185dba70 | ||
|
|
444c4b4434 | ||
|
|
d629d66bf2 | ||
|
|
9d46d68150 | ||
|
|
a4ed6a362e | ||
|
|
a1a0d3cd96 | ||
|
|
950c401e88 | ||
|
|
ce056c9ad2 | ||
|
|
3bd6e454d0 | ||
|
|
f52129a11e | ||
|
|
ea01050163 | ||
|
|
c1ec9b6e99 | ||
|
|
2c509b00ac | ||
|
|
ef358fe741 | ||
|
|
56e0520d9c | ||
|
|
1f7b7f5c41 | ||
|
|
471fa75155 | ||
|
|
c89d16e529 | ||
|
|
fc02ab820a | ||
|
|
ac23cdcc7a | ||
|
|
02c4c5be29 | ||
|
|
3bd7d61e3a | ||
|
|
32ebc69c0e | ||
|
|
c98c3b16c5 | ||
|
|
287fe3adfe | ||
|
|
d4e7b7b371 | ||
|
|
ad78f068e0 | ||
|
|
d156de2e66 | ||
|
|
99bca1bd9b | ||
|
|
8efb22595c | ||
|
|
73e8913f90 | ||
|
|
3ddaf795f2 | ||
|
|
a18df02d37 | ||
|
|
5a3cd93a09 | ||
|
|
d989eba0e1 | ||
|
|
00f8ee9c85 | ||
|
|
01b14d6d30 | ||
|
|
35dbca7bd1 | ||
|
|
73579ec53d | ||
|
|
90ae870a93 | ||
|
|
e9af062ff1 | ||
|
|
60a6c97e22 | ||
|
|
cd8474465b | ||
|
|
2d37b8b0bf | ||
|
|
05c2caa4d9 | ||
|
|
535a8c543f | ||
|
|
dc90cf9ce5 | ||
|
|
36354761cc | ||
|
|
f53add81f3 | ||
|
|
bca59ebd40 | ||
|
|
dcb5da42b0 | ||
|
|
b346c22b57 | ||
|
|
8be350e35f | ||
|
|
6695fb6f61 | ||
|
|
be5b0bb461 | ||
|
|
5f93278326 | ||
|
|
488644f50d | ||
|
|
56bab26018 | ||
|
|
ca42f3851d | ||
|
|
e4bddc2db0 | ||
|
|
8aeacaa001 | ||
|
|
80435d3e5b | ||
|
|
d6c5415c9a | ||
|
|
0306763eeb | ||
|
|
e4ac1288a9 | ||
|
|
e1f489838c | ||
|
|
9b046146a0 | ||
|
|
e0d605582e | ||
|
|
21b5a01cab | ||
|
|
955ea6485f | ||
|
|
9f8a1af7e3 | ||
|
|
e617a3ba40 | ||
|
|
fb3f962039 | ||
|
|
64da54f17c | ||
|
|
74741a1be6 | ||
|
|
e9aaf7024a | ||
|
|
e3d96385ee | ||
|
|
dc266a613e | ||
|
|
1b5755664c | ||
|
|
02bd8abcd1 | ||
|
|
a6e96f5ed1 | ||
|
|
872aa48675 | ||
|
|
60efe3cbb7 | ||
|
|
167f86c283 | ||
|
|
02d14a6359 | ||
|
|
a6a473ee5c | ||
|
|
8be469680d | ||
|
|
45e84d3f9c | ||
|
|
d6d71587c7 | ||
|
|
6b70692dd4 | ||
|
|
a056f241b5 | ||
|
|
115ce8d7c6 | ||
|
|
6e58c13ef5 | ||
|
|
65e29a021b | ||
|
|
6c91f01226 | ||
|
|
36a9ebef8c | ||
|
|
5c921fb983 | ||
|
|
98699b08ad | ||
|
|
1f3d1d1aee | ||
|
|
fc60d9f7d1 | ||
|
|
ad7099ad38 | ||
|
|
2ea9301c5e | ||
|
|
b9b4492de9 | ||
|
|
d74b5c49d9 | ||
|
|
27545ea4b6 | ||
|
|
db3655c843 | ||
|
|
10a10c8ed9 | ||
|
|
800a5fc6be | ||
|
|
924488c29b | ||
|
|
dae050ecb3 | ||
|
|
3a31cf33e2 | ||
|
|
66a0d5d840 | ||
|
|
26caf857bf | ||
|
|
fa34eae800 | ||
|
|
644b16580d | ||
|
|
479dac8397 | ||
|
|
c097b5b831 | ||
|
|
78722f3e73 | ||
|
|
7f8a270770 | ||
|
|
4ff14c8731 | ||
|
|
a957973105 | ||
|
|
f0b855d87e | ||
|
|
446649735d | ||
|
|
397c237e30 | ||
|
|
e10d6b6384 | ||
|
|
38c186d5a7 | ||
|
|
55c320a88a | ||
|
|
6ec0a22194 | ||
|
|
7bae030713 | ||
|
|
7b4714a22a | ||
|
|
332d6abee7 | ||
|
|
4df13cf0b7 | ||
|
|
512deae883 | ||
|
|
2d11477037 | ||
|
|
5e15969f4a | ||
|
|
37e811d7e5 | ||
|
|
a35a1d1ba6 | ||
|
|
41a9c89d71 | ||
|
|
d469102cee | ||
|
|
c685bdd373 | ||
|
|
daa7ae4d73 | ||
|
|
c5caaeba60 | ||
|
|
a473d79554 | ||
|
|
1b5aa106ab | ||
|
|
d8b5653224 | ||
|
|
4cc4ff6c4b | ||
|
|
48a3c57e5f | ||
|
|
efdefc300e | ||
|
|
9730a4f55d | ||
|
|
fc62241332 | ||
|
|
0846001b6d | ||
|
|
90658722e1 | ||
|
|
240752c6cd | ||
|
|
429d4547e4 | ||
|
|
e278b5f74a | ||
|
|
a723058bc1 | ||
|
|
59fa7d5de3 | ||
|
|
60578189bd | ||
|
|
82cd11e01e | ||
|
|
4a10540d4a | ||
|
|
aeda262fb0 | ||
|
|
dff3903617 | ||
|
|
1399caf003 | ||
|
|
2dfb6bc528 | ||
|
|
a392395876 | ||
|
|
5542311c95 | ||
|
|
21e6bb6eec | ||
|
|
bfa7874108 | ||
|
|
423958c899 | ||
|
|
cb380a2031 | ||
|
|
f4e89efdb4 | ||
|
|
f5cb7a1dbd | ||
|
|
72e601948c | ||
|
|
29e30981ae | ||
|
|
06a61e6a22 | ||
|
|
1c83020b43 | ||
|
|
fa4a0ac2d5 | ||
|
|
2addf57cbd | ||
|
|
d521fd20ec | ||
|
|
38d44ac586 | ||
|
|
4768306f53 | ||
|
|
6c6b527dfc | ||
|
|
e14c9bd1b7 | ||
|
|
a2bdc4031b | ||
|
|
8f38eb9e3a | ||
|
|
c22aa58904 | ||
|
|
067bddf892 | ||
|
|
7e4bccbc29 | ||
|
|
f39bb35dc8 | ||
|
|
0d760c04b7 | ||
|
|
6f113542f5 | ||
|
|
1e4c342703 | ||
|
|
3ce0585d7e | ||
|
|
5bf6dd6361 | ||
|
|
929abdff69 | ||
|
|
f295d69e76 | ||
|
|
32a4c55e4a | ||
|
|
615ba91bdb | ||
|
|
dfb2f89a36 | ||
|
|
c9783234cc | ||
|
|
0513e6c2de | ||
|
|
3cdc0234ef | ||
|
|
e06d12be07 | ||
|
|
56394881fc | ||
|
|
61056240c4 | ||
|
|
a59e7c548c | ||
|
|
f63d7674e2 | ||
|
|
fa3a70e9a9 | ||
|
|
b817a8d122 | ||
|
|
2a6668cef3 | ||
|
|
a802821ae9 | ||
|
|
9e13141f6b | ||
|
|
4b64ce2534 | ||
|
|
c550069e11 | ||
|
|
1e10e9c89c | ||
|
|
cd6c1c0e42 | ||
|
|
5edcee9eb9 | ||
|
|
d41c06b1fd | ||
|
|
2a2c5abc81 | ||
|
|
ccdd648603 | ||
|
|
5c1ea04970 | ||
|
|
5ebd22f088 | ||
|
|
5aec091156 | ||
|
|
68993cc21f | ||
|
|
404fc2b36b | ||
|
|
7bb06d0bfa | ||
|
|
abbb64b9c7 | ||
|
|
99ca582c1a | ||
|
|
3e4cfc1573 | ||
|
|
5437e1b7e9 | ||
|
|
4d33cd7d69 | ||
|
|
cb82e94b53 | ||
|
|
dbe14ccedf | ||
|
|
841edf2d24 | ||
|
|
0c375cc50c | ||
|
|
50072887d0 | ||
|
|
976814d846 | ||
|
|
c22024cf66 | ||
|
|
829a219b9f | ||
|
|
13d572ab28 | ||
|
|
c0c2ffa122 | ||
|
|
84c563fda3 | ||
|
|
97720d6a46 | ||
|
|
386982d9c4 | ||
|
|
cb38a2a148 | ||
|
|
07f19209bf | ||
|
|
69c0ca13fc | ||
|
|
e49c50f55c | ||
|
|
f0105f0094 | ||
|
|
3c89afea4a | ||
|
|
848f574f3f | ||
|
|
ec9e34cea4 | ||
|
|
f9069df4e6 | ||
|
|
9035f3eb7e | ||
|
|
e197e788d1 | ||
|
|
49d33522a8 | ||
|
|
06f0ec3c0b | ||
|
|
54319c6949 | ||
|
|
86f875ec3e | ||
|
|
754a6ddb12 | ||
|
|
62e8f75b96 | ||
|
|
c0deb663a6 | ||
|
|
ce28add152 | ||
|
|
5f89315947 | ||
|
|
56dd194a1a | ||
|
|
adcdcc4c8b | ||
|
|
7fd45f089d | ||
|
|
65d82b2080 | ||
|
|
f771935e20 | ||
|
|
70c255fc4f | ||
|
|
bd1396fc54 | ||
|
|
cba5395d5c | ||
|
|
b42c09d919 | ||
|
|
b7af2356fa | ||
|
|
22d477e55f | ||
|
|
dfa69c27bb | ||
|
|
6b78158db2 | ||
|
|
c92cac0eed | ||
|
|
b2dce920fa | ||
|
|
44cec762fb | ||
|
|
a98188ead8 | ||
|
|
294b1a469f | ||
|
|
ebed588c1c | ||
|
|
46f8217e1a | ||
|
|
9ab23cf485 | ||
|
|
7790660fe8 | ||
|
|
1dd2726beb | ||
|
|
ba9ce89d1b | ||
|
|
c9082ac709 | ||
|
|
27c8fa8986 | ||
|
|
a04289d71b | ||
|
|
321859e0f5 | ||
|
|
4b4bf413c1 | ||
|
|
06c4f75b4a | ||
|
|
12dd97af61 | ||
|
|
8e77197459 | ||
|
|
3cc64cada6 | ||
|
|
3ac5874df1 | ||
|
|
107dd6f360 | ||
|
|
347e0cd336 | ||
|
|
2ba6058ec7 | ||
|
|
683aa258b8 | ||
|
|
d7ad7e314d | ||
|
|
ef66eb1c31 | ||
|
|
ec8b256852 | ||
|
|
5a715b2fb5 | ||
|
|
e80578e682 | ||
|
|
155a73dc39 | ||
|
|
f5d0b50d90 | ||
|
|
b0745de672 | ||
|
|
d54fdbf84f | ||
|
|
0a6432c393 | ||
|
|
9e91c382b3 | ||
|
|
2cf24761c0 | ||
|
|
c2bdc31a03 | ||
|
|
9d487d7b61 | ||
|
|
a2ab6c196a | ||
|
|
6a5b02e8ad | ||
|
|
e76f214024 | ||
|
|
cb47717b09 | ||
|
|
85dbc9a6ca | ||
|
|
4aebeaffa7 | ||
|
|
6a84e2d5b6 | ||
|
|
e26620e350 | ||
|
|
1cabd2a514 | ||
|
|
06dd70078e | ||
|
|
4b13866e1d | ||
|
|
11142fda25 | ||
|
|
b4057f9588 | ||
|
|
9524f030d1 | ||
|
|
3f10dce04f | ||
|
|
5f8802272d | ||
|
|
0d486c2774 | ||
|
|
e78a1583c0 | ||
|
|
ea966c48b9 | ||
|
|
810b8d46b9 | ||
|
|
8a00b74863 | ||
|
|
94fafe661d | ||
|
|
7d58a124ab | ||
|
|
378a8d95f9 | ||
|
|
3a2e39af97 | ||
|
|
6f6d725126 | ||
|
|
3d8eb34d80 | ||
|
|
2768fc02ea | ||
|
|
0902467fa6 | ||
|
|
1dc999070f | ||
|
|
7a169715ea | ||
|
|
361f6781ee | ||
|
|
42190a89dd | ||
|
|
2d4c6a1d3b | ||
|
|
1dd88795c3 | ||
|
|
7cff7311e1 | ||
|
|
bf93959350 | ||
|
|
94151c2668 | ||
|
|
bf650e6d4c | ||
|
|
575f195970 | ||
|
|
c6bf6c92c1 | ||
|
|
b86c31bff8 | ||
|
|
fc37bbec1d | ||
|
|
6525c66600 | ||
|
|
145234c5c3 | ||
|
|
a7f816f49a | ||
|
|
694b0a5381 | ||
|
|
8a0947faf3 | ||
|
|
d1c4b20160 | ||
|
|
d81d8419a0 | ||
|
|
c6acdab7c6 | ||
|
|
0374143148 | ||
|
|
2d3c5ed761 | ||
|
|
a611451233 | ||
|
|
93dcd8f16e | ||
|
|
294519c7a5 | ||
|
|
f11df1f513 | ||
|
|
563609e10a | ||
|
|
b4d4e36f72 | ||
|
|
f291efc428 | ||
|
|
1a61ba3cc7 | ||
|
|
3d10cea137 | ||
|
|
6c72e9dad4 | ||
|
|
c5d4f6b94d | ||
|
|
c52d7b6de5 | ||
|
|
6ade0a837f | ||
|
|
81d69c8e72 | ||
|
|
ca333e895f | ||
|
|
8e527efd07 | ||
|
|
c31c03f5a9 | ||
|
|
fe34acf314 | ||
|
|
1f21a874b8 | ||
|
|
ee6a6f0d2d | ||
|
|
8d0181ccca | ||
|
|
e8dba05920 | ||
|
|
6652e2f15c | ||
|
|
578eec8a2d | ||
|
|
8b116d2234 | ||
|
|
b9aa110440 | ||
|
|
9f50bbda79 | ||
|
|
6f2ce69b77 | ||
|
|
8ad2678ce2 | ||
|
|
49c91262fd | ||
|
|
15378682ab | ||
|
|
296861ce3a | ||
|
|
aa59acf0bc | ||
|
|
83e204f3f8 | ||
|
|
497d60e244 | ||
|
|
e8282d6d4a | ||
|
|
72510047d8 | ||
|
|
7dee21eb72 | ||
|
|
d06290642b | ||
|
|
525abe6b88 | ||
|
|
a7704edb9c | ||
|
|
78693f4fc6 | ||
|
|
186484defa | ||
|
|
f8fe704c42 | ||
|
|
82857db236 | ||
|
|
661914b0db | ||
|
|
057299de2b | ||
|
|
93db1a23e2 | ||
|
|
36ec03b24b | ||
|
|
1d6f47c49e | ||
|
|
10dfab7127 | ||
|
|
a8ebe2c096 | ||
|
|
b3dc1c1513 | ||
|
|
7cdae09a94 | ||
|
|
59c2c2fd5d | ||
|
|
70000aab75 | ||
|
|
059d79302d | ||
|
|
ee300466aa | ||
|
|
c4fb3f72e5 | ||
|
|
ed0da96076 | ||
|
|
85fbc54384 | ||
|
|
a6c282520a | ||
|
|
aa4faba4a3 | ||
|
|
5e864c4ff1 | ||
|
|
0c4e612e39 | ||
|
|
92faa846ac | ||
|
|
5bfdd8e1ef | ||
|
|
5af93a57c7 | ||
|
|
8549bf2fcf | ||
|
|
762da62a29 | ||
|
|
7d536e6cdb | ||
|
|
166d7953c9 | ||
|
|
8ea457f949 | ||
|
|
35dfc720c8 | ||
|
|
3ca2739fce | ||
|
|
5df3d8f6e2 | ||
|
|
980a4c4003 | ||
|
|
c3f5374c6e | ||
|
|
fcccc19fc5 | ||
|
|
c2609a3316 | ||
|
|
34f3240c03 | ||
|
|
dba4792a7d | ||
|
|
ad13f7e5c6 | ||
|
|
20476b9445 | ||
|
|
21ad3d78a4 | ||
|
|
8b8e41fa1b | ||
|
|
f997cd0b47 | ||
|
|
9241be296f | ||
|
|
47c2d5c15e | ||
|
|
ed89d8546c | ||
|
|
19de32cb5d | ||
|
|
08a859c002 | ||
|
|
ff1db82c56 | ||
|
|
c122afefb9 | ||
|
|
689542947e | ||
|
|
f64aeff6b5 | ||
|
|
4dd12f6230 | ||
|
|
4571d35102 | ||
|
|
ed7aca7bc5 | ||
|
|
f96dc974f3 | ||
|
|
81f3dbf584 | ||
|
|
4f91b30ee1 | ||
|
|
51272c0fc4 | ||
|
|
27d4a26ae8 | ||
|
|
2fdf630ed3 | ||
|
|
dc056a6cc0 | ||
|
|
54015223a2 | ||
|
|
345777cb72 | ||
|
|
347493fe6b | ||
|
|
766438dcf3 | ||
|
|
f2989e836a | ||
|
|
34118bfc90 | ||
|
|
91a74e309a | ||
|
|
0c32b98e85 | ||
|
|
e059fab172 | ||
|
|
08a8bc0e4f | ||
|
|
3dc0cd97ca | ||
|
|
697adecc1d | ||
|
|
1063945825 | ||
|
|
8aaf4c676f | ||
|
|
10e6abad18 | ||
|
|
a69aa06123 | ||
|
|
81a5857d07 | ||
|
|
cc5cc30279 | ||
|
|
0d492cc9d9 | ||
|
|
5440de3684 | ||
|
|
42768f1f09 | ||
|
|
0d168d79b2 | ||
|
|
c90b8b702b | ||
|
|
db64ff2098 | ||
|
|
1d96fa6146 | ||
|
|
ce72bfdb68 | ||
|
|
7ac8f6a40a | ||
|
|
1cb0cfa113 | ||
|
|
e2899e66d1 | ||
|
|
7229d87fd1 | ||
|
|
fcbabe95ea | ||
|
|
4a0b23b7a1 | ||
|
|
f9190549ff | ||
|
|
79774b1d47 | ||
|
|
fce5b23763 | ||
|
|
0af61b1387 | ||
|
|
e48ab11d5b | ||
|
|
9f92c412b4 | ||
|
|
2b30e0998c | ||
|
|
b8443d517e | ||
|
|
e7dd047eb5 | ||
|
|
3541c66637 | ||
|
|
028e0b0dfc | ||
|
|
1ce7962fa5 | ||
|
|
70a20726a9 | ||
|
|
f83c24c020 | ||
|
|
d4bfdf699b | ||
|
|
1ee501f8b9 | ||
|
|
13d67eb2a3 | ||
|
|
a9558cb296 | ||
|
|
7706d44667 | ||
|
|
3486aead7f | ||
|
|
2947784f00 | ||
|
|
7acb102b43 | ||
|
|
4bd1f3de7d | ||
|
|
5f2570c440 | ||
|
|
f066880c7c | ||
|
|
1eded91f24 | ||
|
|
029f201a46 | ||
|
|
6f5a6f6500 | ||
|
|
9c555c06be | ||
|
|
9198b122ec | ||
|
|
5a3ee877d6 | ||
|
|
3c049ab65a |
30
.env
30
.env
@@ -1,30 +0,0 @@
|
||||
NODE_ENV='production'
|
||||
ACCESS_TOKEN_COOKIE_NAME=null
|
||||
BASE_URL=null
|
||||
CREDENTIALS_BASE_URL=null
|
||||
CSRF_TOKEN_API_PATH=null
|
||||
ECOMMERCE_BASE_URL=null
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=null
|
||||
LMS_BASE_URL=null
|
||||
LOGIN_URL=null
|
||||
LOGOUT_URL=null
|
||||
MARKETING_SITE_BASE_URL=null
|
||||
ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
USER_SURVEY_COOKIE_NAME=null
|
||||
COOKIE_DOMAIN=null
|
||||
WELCOME_PAGE_SUPPORT_LINK=null
|
||||
INFO_EMAIL=''
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||
ENABLE_PROGRESSIVE_PROFILING=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
SHOW_DYNAMIC_PROFILING_PAGE=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
@@ -18,18 +18,26 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
INFO_EMAIL='info@example.com'
|
||||
# ***** Features *****
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS='true'
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
TOS_LINK='http://localhost:18000/tos'
|
||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
COOKIE_DOMAIN='localhost'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
INFO_EMAIL='info@edx.org'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE=''
|
||||
BANNER_IMAGE_MEDIUM=''
|
||||
BANNER_IMAGE_SMALL=''
|
||||
BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
|
||||
28
.env.test
28
.env.test
@@ -1,28 +0,0 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='http://localhost:1995'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
LOGIN_ISSUE_SUPPORT_LINK='https://login-issue-support-url.com'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
@@ -1,5 +0,0 @@
|
||||
coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
24
.eslintrc.js
24
.eslintrc.js
@@ -1,24 +0,0 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
rules: {
|
||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
||||
// to fail for no apparent reason since upgrading
|
||||
// @edx/frontend-build from v3 to v5:
|
||||
// - TypeError: Cannot read property 'range' of null
|
||||
'indent': [
|
||||
'error',
|
||||
2,
|
||||
{ 'ignoredNodes': ['TemplateLiteral', 'SwitchCase'] }
|
||||
],
|
||||
'template-curly-spacing': 'off',
|
||||
'jsx-a11y/label-has-associated-control': ['error', {
|
||||
labelComponents: [],
|
||||
labelAttributes: [],
|
||||
controlComponents: [],
|
||||
assert: 'htmlFor',
|
||||
depth: 25
|
||||
}],
|
||||
},
|
||||
});
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @openedx/2U-infinity
|
||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
29
.github/pull_request_template.md
vendored
Normal file
29
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
### Description
|
||||
|
||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or Github issues.
|
||||
|
||||
#### JIRA
|
||||
|
||||
[XXX-XXXX](https://2u-internal.atlassian.net/browse/XXX-XXXX)
|
||||
|
||||
#### How Has This Been Tested?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### Screenshots/sandbox (optional):
|
||||
|
||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if its not applicable.**
|
||||
|
||||
|Before|After|
|
||||
|-------|-----|
|
||||
| | |
|
||||
|
||||
#### Merge Checklist
|
||||
|
||||
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||
* [ ] Is there adequate test coverage for your changes?
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
25
.github/workflows/autoupdate-pull-request.yml
vendored
Normal file
25
.github/workflows/autoupdate-pull-request.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: autoupdate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [ labeled ]
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
autoupdate:
|
||||
name: autoupdate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
DRY_RUN: "false"
|
||||
PR_FILTER: "labelled"
|
||||
PR_LABELS: "autoupdate"
|
||||
EXCLUDED_LABELS: "dependencies,wontfix"
|
||||
MERGE_MSG: "Branch was auto-updated."
|
||||
RETRY_COUNT: "5"
|
||||
RETRY_SLEEP: "300"
|
||||
MERGE_CONFLICT_ACTION: "fail"
|
||||
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -10,18 +10,15 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
@@ -41,8 +38,8 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify Es5
|
||||
run: npm run is-es5
|
||||
|
||||
- name: Run Code Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,19 +1,15 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
.idea
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
module.config.js
|
||||
|
||||
dist/
|
||||
/*.tgz
|
||||
|
||||
### i18n ###
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
### Emacs ###
|
||||
### Editors ###
|
||||
.DS_Store
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
|
||||
15
.npmignore
15
.npmignore
@@ -1,11 +1,6 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
coverage
|
||||
__mocks__
|
||||
node_modules
|
||||
public
|
||||
*.test.js
|
||||
*.test.jsx
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-authn]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @edx/vanguards
|
||||
* @openedx/2u-infinity
|
||||
|
||||
45
Makefile
Executable file → Normal file
45
Makefile
Executable file → Normal file
@@ -1,19 +1,17 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-authn
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
precommit:
|
||||
npm run lint
|
||||
npm audit
|
||||
|
||||
requirements:
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -31,22 +29,31 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
|
||||
|
||||
# This target is used by CI.
|
||||
$(intl_imports) paragon frontend-platform frontend-app-authn
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
.PHONY: validate
|
||||
validate:
|
||||
make validate-no-uncommitted-package-lock-changes
|
||||
npm run i18n_extract
|
||||
npm run lint -- --max-warnings 0
|
||||
npm run test
|
||||
npm run build
|
||||
|
||||
.PHONY: validate.ci
|
||||
validate.ci:
|
||||
npm ci
|
||||
make validate
|
||||
|
||||
204
README.rst
204
README.rst
@@ -1,48 +1,204 @@
|
||||
|Build Status| |Codecov| |license|
|
||||
|
||||
##################
|
||||
frontend-app-authn
|
||||
=================================
|
||||
##################
|
||||
|
||||
|Build Status| |ci-badge| |Codecov| |semantic-release|
|
||||
|
||||
********
|
||||
Purpose
|
||||
********
|
||||
|
||||
This is a micro-frontend application responsible for the login, registration and password reset functionality.
|
||||
|
||||
Development
|
||||
-----------
|
||||
**What is the domain of this MFE?**
|
||||
|
||||
Start Devstack
|
||||
^^^^^^^^^^^^^^
|
||||
- Register page
|
||||
|
||||
To use this application `devstack <https://github.com/edx/devstack>`__ must be running.
|
||||
- Login page
|
||||
|
||||
- Start devstack
|
||||
- Forgot password page
|
||||
|
||||
Start the development server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Reset password page
|
||||
|
||||
In this project, install requirements and start the development server by running:
|
||||
- Progressive profiling page
|
||||
|
||||
.. code:: bash
|
||||
***************
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1999
|
||||
Installation
|
||||
============
|
||||
|
||||
Once the dev server is up visit http://localhost:1999/login.
|
||||
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
Configuration and Deployment
|
||||
----------------------------
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
|
||||
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
|
||||
Environment Variables/Setup Notes
|
||||
=================================
|
||||
|
||||
.. code:: bash
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
|
||||
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||
The authentication micro-frontend also requires the following additional variable:
|
||||
|
||||
.. list-table:: Environment Variables
|
||||
:widths: 30 50 20
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Description / Usage
|
||||
- Example
|
||||
|
||||
* - ``LOGIN_ISSUE_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the login issue support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``ACTIVATION_EMAIL_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the activation email support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``PASSWORD_RESET_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the password reset support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the progressive profiling support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``TOS_AND_HONOR_CODE``
|
||||
- The fully-qualified URL to the Honor code page in the target environment.
|
||||
- ``https://example.com/honor``
|
||||
|
||||
* - ``TOS_LINK``
|
||||
- The fully-qualified URL to the Terms of service page in the target environment.
|
||||
- ``https://example.com/tos``
|
||||
|
||||
* - ``PRIVACY_POLICY``
|
||||
- The fully-qualified URL to the Privacy policy page in the target environment.
|
||||
- ``https://example.com/privacy``
|
||||
|
||||
* - ``INFO_EMAIL``
|
||||
- The valid email address for information query regarding the target environment.
|
||||
- ``info@example.com``
|
||||
|
||||
* - ``ENABLE_DYNAMIC_REGISTRATION_FIELDS``
|
||||
- Enables support for configurable registration fields on the MFE. This flag must be enabled to show any required registration field besides the default fields (name, email, username, password).
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN``
|
||||
- Enables support for progressive profiling. If enabled, users are redirected to a second page where data for optional registration fields can be collected.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``DISABLE_ENTERPRISE_LOGIN``
|
||||
- Disables the enterprise login from Authn MFE.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``MFE_CONFIG_API_URL``
|
||||
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
|
||||
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``APP_ID``
|
||||
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
|
||||
- ``authn`` | ``''``
|
||||
|
||||
* - ``ENABLE_IMAGE_LAYOUT``
|
||||
- Enables the image layout feature within the authn. When set to True, this feature allows the inclusion of images in the base container layout. For more details on configuring this feature, please refer to the `Modifying base container <docs/how_tos/modifying_base_container.rst>`_.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
|
||||
edX-specific Environment Variables
|
||||
==================================
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and might be unsupported in Open edX.
|
||||
|
||||
.. list-table:: edX-specific Environment Variables
|
||||
:widths: 30 50 20
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Description / Usage
|
||||
- Example
|
||||
|
||||
* - ``MARKETING_EMAILS_OPT_IN``
|
||||
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
|
||||
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
|
||||
How To Contribute
|
||||
=================
|
||||
|
||||
Contributions are very welcome, and strongly encouraged! We've
|
||||
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
|
||||
|
||||
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
|
||||
|
||||
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
|
||||
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-authn/blob/master/.github/pull_request_template.md>`_
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes and security fixes.
|
||||
|
||||
Getting Help
|
||||
============
|
||||
|
||||
If you're having trouble, we have discussion forums at
|
||||
https://discuss.openedx.org where you can connect with others in the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-authn/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
The Open edX Code of Conduct
|
||||
============================
|
||||
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
|
||||
|
||||
People
|
||||
======
|
||||
The assigned maintainers for this component and other project details may be
|
||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||
file in this repo.
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
Known Issues
|
||||
============
|
||||
|
||||
None
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
|
||||
otherwise noted.
|
||||
|
||||
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
|
||||
|
||||
|
||||
==============================
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master
|
||||
:target: https://travis-ci.com/edx/frontend-app-authn
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-authn
|
||||
:target: https://codecov.io/gh/edx/frontend-app-authn
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-authn.svg
|
||||
:target: @edx/frontend-app-authn
|
||||
.. |ci-badge| image:: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
|
||||
5
app.d.ts
vendored
Normal file
5
app.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="@openedx/frontend-base" />
|
||||
|
||||
declare module 'site.config' {
|
||||
export default SiteConfig;
|
||||
}
|
||||
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { createConfig } = require('@openedx/frontend-base/config');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
19
catalog-info.yaml
Normal file
19
catalog-info.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-authn'
|
||||
description: "Micro-frontend for authentication service. It contains views for login, registration and password reset functionality."
|
||||
links:
|
||||
- url: 'https://github.com/openedx/frontend-app-authn/blob/master/README.rst'
|
||||
title: 'Documentation'
|
||||
icon: 'Article'
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
@@ -91,7 +91,7 @@ In the data sub-directory, the file names describe what each piece of code does.
|
||||
/ProfilePhotoUploader.jsx // supporting view
|
||||
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
||||
/actions.js
|
||||
/constants.js
|
||||
/mockedData.js
|
||||
/reducers.js
|
||||
/sagas.js
|
||||
/selectors.js
|
||||
|
||||
14
docs/how_tos/enable_social_auth.rst
Normal file
14
docs/how_tos/enable_social_auth.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Enable Social Auth Locally
|
||||
--------------------------
|
||||
|
||||
Please follow the steps below to enable social auth (SSO) locally.
|
||||
|
||||
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
|
||||
|
||||
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.
|
||||
|
||||
* If the provider has an ``iconImage``, then it will be rendered as image in SSO button.
|
||||
|
||||
* If ``iconImage`` is not available in provider, but the provider's ``iconClass`` is from the supported icon classes ``['apple', 'facebook', 'google', 'microsoft']`` then it is used as icon image.
|
||||
|
||||
* If ``iconClass`` doesn't match the supported icon classes then the ``faSignInAlt`` from font awesome icons is used as icon image for SSO button.
|
||||
@@ -2,4 +2,4 @@
|
||||
React App i18n HOWTO
|
||||
####################
|
||||
|
||||
This document has moved to the frontend-platform repo: https://github.com/edx/frontend-platform/blob/master/docs/how_tos/i18n.rst
|
||||
This document has moved to the frontend-platform repo: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
|
||||
|
||||
39
docs/how_tos/modifying_base_container.rst
Normal file
39
docs/how_tos/modifying_base_container.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
========================================
|
||||
Modifying the Base Container in Authn
|
||||
========================================
|
||||
|
||||
The base container in Authn serves as the fundamental layout structure for rendering different components based on configurations. This document outlines the process for modifying the base container to accommodate changes or customize layouts as needed.
|
||||
|
||||
Understanding Base Container Versions
|
||||
--------------------------------------
|
||||
|
||||
The base container supports two main versions:
|
||||
|
||||
- **Default Layout:** The default layout is the standard layout used when specific configurations do not dictate otherwise.
|
||||
.. image:: ../images/default_layout.png
|
||||
- **Image Layout:** The image layout is an alternative layout option that can be enabled based on configurations.
|
||||
.. image:: ../images/image_layout.png
|
||||
|
||||
Enabling the Image Layout
|
||||
---------------------------
|
||||
|
||||
To activate the image layout feature, navigate to your .env file and update the configurations:
|
||||
|
||||
**Update Configuration**
|
||||
|
||||
Locate the ``ENABLE_IMAGE_LAYOUT`` parameter and set its value to ``true``. Additionally, ensure that the Image configuration settings are provided. Your overall configurations should resemble the following:
|
||||
|
||||
|
||||
.. code-block::
|
||||
|
||||
# ***** Image Layout Configuration *****
|
||||
ENABLE_IMAGE_LAYOUT = True # Set to True to enable image layout feature
|
||||
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE='' # Path to the large banner image
|
||||
BANNER_IMAGE_MEDIUM='' # Path to the medium-sized banner image
|
||||
BANNER_IMAGE_SMALL='' # Path to the small banner image
|
||||
BANNER_IMAGE_EXTRA_SMALL='' # Path to the extra-small banner image
|
||||
|
||||
|
||||
This allows for the customization and adaptation of the base container layout according to specific requirements.
|
||||
BIN
docs/images/default_layout.png
Normal file
BIN
docs/images/default_layout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
BIN
docs/images/frontend-app-authn-localhost-preview.png
Normal file
BIN
docs/images/frontend-app-authn-localhost-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
BIN
docs/images/image_layout.png
Normal file
BIN
docs/images/image_layout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
22
eslint.config.js
Normal file
22
eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @ts-check
|
||||
|
||||
const { createLintConfig } = require('@openedx/frontend-base/config');
|
||||
|
||||
module.exports = createLintConfig(
|
||||
{
|
||||
files: [
|
||||
'src/**/*',
|
||||
'site.config.*',
|
||||
],
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'coverage/*',
|
||||
'dist/*',
|
||||
'docs/*',
|
||||
'node_modules/*',
|
||||
'**/__mocks__/*',
|
||||
'**/__snapshots__/*',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -1,13 +1,15 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-base/config');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
module.exports = createConfig('test', {
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/setupTest.js',
|
||||
'src/i18n',
|
||||
'src/index.jsx',
|
||||
'MainApp.jsx',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: edx/vanguards
|
||||
openedx-release:
|
||||
ref: master
|
||||
60285
package-lock.json
generated
60285
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
133
package.json
133
package.json
@@ -1,92 +1,79 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-authn",
|
||||
"version": "0.1.0",
|
||||
"description": "Frontend application template",
|
||||
"name": "@openedx/frontend-app-authn",
|
||||
"version": "1.0.0-alpha.0",
|
||||
"description": "Frontend authentication",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-authn.git"
|
||||
"url": "git+https://github.com/openedx/frontend-app-authn.git"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"/src"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"*.scss"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
"dev": "PORT=1999 PUBLIC_PATH=/authn openedx dev",
|
||||
"i18n_extract": "openedx formatjs extract",
|
||||
"lint": "openedx lint .",
|
||||
"lint:fix": "openedx lint --fix .",
|
||||
"snapshot": "openedx test --updateSnapshot",
|
||||
"test": "openedx test --coverage --passWithNoTests"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"author": "edX",
|
||||
"author": "Open edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-authn#readme",
|
||||
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-app-authn/issues"
|
||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.1.14",
|
||||
"@edx/frontend-platform": "1.15.5",
|
||||
"@edx/paragon": "19.10.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.10",
|
||||
"core-js": "3.21.1",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
"fastest-levenshtein": "1.0.12",
|
||||
"form-urlencoded": "4.2.1",
|
||||
"formik": "2.2.9",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "5.1.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "2.2.0",
|
||||
"react-onclickoutside": "6.12.1",
|
||||
"react-redux": "7.2.6",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"redux": "4.1.2",
|
||||
"@redux-devtools/extension": "3.2.2",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.4.1",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"reselect": "4.1.5",
|
||||
"sanitize-html": "2.7.0",
|
||||
"semver-regex": "3.1.4",
|
||||
"universal-cookie": "4.0.4"
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"classnames": "^2.5.1",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"form-urlencoded": "^6.1.5",
|
||||
"i18n-iso-countries": "^7.13.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^7.1.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-responsive": "^8.2.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-mock-store": "^1.5.5",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"reselect": "^5.1.1",
|
||||
"universal-cookie": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "9.1.4",
|
||||
"@edx/reactifex": "1.0.3",
|
||||
"babel-plugin-formatjs": "10.3.18",
|
||||
"codecov": "3.8.2",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"es-check": "6.2.1",
|
||||
"glob": "7.2.0",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"react-test-renderer": "16.14.0"
|
||||
"@edx/browserslist-config": "^1.5.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"babel-plugin-formatjs": "10.5.38",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"jest": "^29.7.0",
|
||||
"react-test-renderer": "^18.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@openedx/frontend-base": "^1.0.0-alpha.1",
|
||||
"@openedx/paragon": "^22",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^8",
|
||||
"react-router": "^6",
|
||||
"react-router-dom": "^6",
|
||||
"redux": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,9 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Authn | edX</title>
|
||||
<title>Authentication Development Site></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script
|
||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||
></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
<% if (process.env.ZENDESK_KEY) { %>
|
||||
<script
|
||||
id="ze-snippet"
|
||||
src="https://static.zdassets.com/ekr/snippet.js?key=<%= process.env.ZENDESK_KEY %>"
|
||||
>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
window.zESettings = {
|
||||
cookies: true,
|
||||
webWidget: {
|
||||
contactOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
{
|
||||
id: 360003368814,
|
||||
subject: false,
|
||||
fields: [
|
||||
{
|
||||
id: 'description',
|
||||
prefill: {
|
||||
'*': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectTicketForm: {
|
||||
'*': 'Please choose your request type:',
|
||||
},
|
||||
attachments: true,
|
||||
},
|
||||
helpCenter: {
|
||||
originalArticleButton: true,
|
||||
},
|
||||
answerBot: {
|
||||
suppress: false,
|
||||
contactOnlyAfterQuery: true,
|
||||
title: { '*': 'edX Support' },
|
||||
avatar: {
|
||||
url: '<%= process.env.ZENDESK_LOGO_URL %>',
|
||||
name: { '*': 'edX Support' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
20
site.config.dev.tsx
Normal file
20
site.config.dev.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
|
||||
|
||||
import { authnApp } from './src';
|
||||
|
||||
import './src/app.scss';
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
siteId: 'authn-dev',
|
||||
siteName: 'Authn Dev',
|
||||
baseUrl: 'http://apps.local.openedx.io:8080',
|
||||
lmsBaseUrl: 'http://local.openedx.io:8000',
|
||||
loginUrl: 'http://local.openedx.io:8000/login',
|
||||
logoutUrl: 'http://local.openedx.io:8000/logout',
|
||||
|
||||
environment: EnvironmentTypes.DEVELOPMENT,
|
||||
basename: '/authn',
|
||||
apps: [authnApp],
|
||||
};
|
||||
|
||||
export default siteConfig;
|
||||
50
site.config.test.tsx
Normal file
50
site.config.test.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
|
||||
|
||||
import { appId } from './src/constants';
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
siteId: 'test-site',
|
||||
siteName: 'Test Site',
|
||||
baseUrl: 'http://localhost:1996',
|
||||
lmsBaseUrl: 'http://localhost:8000',
|
||||
loginUrl: 'http://localhost:8000/login',
|
||||
logoutUrl: 'http://localhost:8000/logout',
|
||||
|
||||
environment: EnvironmentTypes.TEST,
|
||||
apps: [{
|
||||
appId,
|
||||
config: {
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: null,
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: null,
|
||||
BANNER_IMAGE_EXTRA_SMALL: '',
|
||||
BANNER_IMAGE_LARGE: '',
|
||||
BANNER_IMAGE_MEDIUM: '',
|
||||
BANNER_IMAGE_SMALL: '',
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: false,
|
||||
ENABLE_IMAGE_LAYOUT: false,
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: false,
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
INFO_EMAIL: '',
|
||||
LOGIN_ISSUE_SUPPORT_LINK: null,
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||
PASSWORD_RESET_SUPPORT_LINK: null,
|
||||
POST_REGISTRATION_REDIRECT_URL: '',
|
||||
PRIVACY_POLICY: null,
|
||||
SEARCH_CATALOG_URL: null,
|
||||
SESSION_COOKIE_DOMAIN: 'local.openedx.io',
|
||||
SHOW_REGISTRATION_LINKS: false,
|
||||
TOS_AND_HONOR_CODE: null,
|
||||
TOS_LINK: null,
|
||||
USER_RETENTION_COOKIE_NAME: '',
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
export default siteConfig;
|
||||
23
src/Main.tsx
Executable file
23
src/Main.tsx
Executable file
@@ -0,0 +1,23 @@
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { CurrentAppProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { appId } from './constants';
|
||||
import {
|
||||
registerIcons,
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
|
||||
import './sass/_style.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const Main = () => (
|
||||
<CurrentAppProvider appId={appId}>
|
||||
<ReduxProvider store={configureStore()}>
|
||||
<Outlet />
|
||||
</ReduxProvider>
|
||||
</CurrentAppProvider>
|
||||
);
|
||||
|
||||
export default Main;
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import {
|
||||
UnAuthOnlyRoute, registerIcons, NotFoundPage, Logistration,
|
||||
} from './common-components';
|
||||
import {
|
||||
LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, WELCOME_PAGE,
|
||||
} from './data/constants';
|
||||
import configureStore from './data/configureStore';
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import WelcomePage, { ProgressiveProfiling } from './welcome';
|
||||
import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const MainApp = () => (
|
||||
<AppProvider store={configureStore()}>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
||||
</Route>
|
||||
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} />
|
||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Route
|
||||
exact
|
||||
path={WELCOME_PAGE}
|
||||
component={(getConfig().SHOW_DYNAMIC_PROFILING_PAGE) ? ProgressiveProfiling : WelcomePage}
|
||||
/>
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
export default MainApp;
|
||||
1
src/__mocks__/file.js
Normal file
1
src/__mocks__/file.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'FileMock';
|
||||
1
src/__mocks__/svg.js
Normal file
1
src/__mocks__/svg.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'SvgURL';
|
||||
887
src/_style.scss
887
src/_style.scss
@@ -1,887 +0,0 @@
|
||||
// ----------------------------
|
||||
// #COLORS
|
||||
// ----------------------------
|
||||
$font-blue: #126f9a;
|
||||
$white: #FFFFFF;
|
||||
|
||||
// social platforms
|
||||
$facebook-blue: #1877F2;
|
||||
$facebook-focus-blue: #29487d;
|
||||
$google-blue: #4285f4;
|
||||
$google-focus-blue: #287ae6;
|
||||
$microsoft-black: #2f2f2f;
|
||||
$microsoft-focus-black: #000;
|
||||
$apple-black: #000000;
|
||||
$apple-focus-black: $apple-black;
|
||||
$accent-a-light: #c9f2f5;
|
||||
|
||||
.centered-align-spinner {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@extend .pt-4;
|
||||
min-width: 464px !important;
|
||||
}
|
||||
|
||||
.welcome-page-content {
|
||||
min-width: 464px !important;
|
||||
}
|
||||
|
||||
.stateful-button-width {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.register-stateful-button-width {
|
||||
min-width: 14.4rem;
|
||||
}
|
||||
|
||||
.login-button-width {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.tpa-skeleton {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close {
|
||||
padding-bottom: 0px !important;
|
||||
padding-top: 3px !important;
|
||||
}
|
||||
|
||||
.focus-out {
|
||||
position: absolute;
|
||||
padding-left: 17px;
|
||||
opacity: 0.75;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
color: #0075b4 !important;
|
||||
|
||||
&:hover {
|
||||
color: #065683 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.authn-header {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
height: 3.75rem;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.authn-header img {
|
||||
height: 1.75rem;
|
||||
margin-left: 2rem;
|
||||
padding: 1rem 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: white !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
height: 2.75rem;
|
||||
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
|
||||
background-color: $white;
|
||||
border: 1px solid $font-blue;
|
||||
width: 224px;
|
||||
height: 36px;
|
||||
color: $font-blue;
|
||||
|
||||
.icon-image {
|
||||
background-color: transparent;
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-tpa {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
width: 14rem;
|
||||
|
||||
.icon-image {
|
||||
background-color: transparent;
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-container {
|
||||
background-color: $font-blue;
|
||||
color: $white;
|
||||
font-size: 11px;
|
||||
|
||||
margin-left: -6px;
|
||||
padding-top: 10px;
|
||||
min-width: 30px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.btn-oa2-facebook {
|
||||
color: $white;
|
||||
border-color: $facebook-blue;
|
||||
background-color: $facebook-blue;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $facebook-focus-blue;
|
||||
border: 1px solid $facebook-focus-blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-google-oauth2 {
|
||||
color: $white;
|
||||
border-color: $google-blue;
|
||||
background-color: $google-blue;
|
||||
|
||||
.icon-image {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $google-focus-blue;
|
||||
border: 1px solid $google-focus-blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-apple-id {
|
||||
color: $white;
|
||||
border-color: $apple-black;
|
||||
background-color: $apple-black;
|
||||
font-size: 16px;
|
||||
|
||||
.icon-image {
|
||||
max-height: 1.8em;
|
||||
max-width: 2.0em;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $apple-focus-black;
|
||||
border: 1px solid $apple-focus-black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-azuread-oauth2 {
|
||||
color: $white;
|
||||
border-color: $microsoft-black;
|
||||
background-color: $microsoft-black;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $microsoft-focus-black;
|
||||
border: 1px solid $microsoft-focus-black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
display: inherit;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.institute-icon {
|
||||
@extend .mr-1;
|
||||
@extend .text-gray;
|
||||
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25rem;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.login-help {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.full-vertical-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.help-links {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
#honor-code p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#honor-code a span {
|
||||
@extend .sr-only;
|
||||
}
|
||||
|
||||
.font-weight-500 {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.mw-420 {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.mw-500 {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.mw-32em {
|
||||
max-width: 32em;
|
||||
}
|
||||
|
||||
.h-90 {
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-7 {
|
||||
margin-top: 7rem;
|
||||
}
|
||||
|
||||
.pt-10 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.tooltip-shadow {
|
||||
box-shadow: 0px 5px 15px 0px rgba(0,0,0,0.3) !important;
|
||||
}
|
||||
#password-requirement-left {
|
||||
opacity: 1;
|
||||
.tooltip-inner {
|
||||
@extend .tooltip-shadow;
|
||||
background: white;
|
||||
display: block;
|
||||
color: #707070;
|
||||
}
|
||||
.arrow::before {
|
||||
border-left-color: #fff;
|
||||
@extend .tooltip-shadow;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#password-requirement-top {
|
||||
opacity: 1;
|
||||
width: 90%;
|
||||
margin-bottom: 10px;
|
||||
.tooltip-inner {
|
||||
max-width: inherit;
|
||||
background: white;
|
||||
display: block;
|
||||
@extend .tooltip-shadow;
|
||||
color: #707070;
|
||||
}
|
||||
.arrow::before {
|
||||
border-top-color: #fff;
|
||||
@extend .tooltip-shadow;
|
||||
}
|
||||
|
||||
}
|
||||
#forgotpassword-success-alert {
|
||||
.alert-link {
|
||||
color: #454545 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-header-light {
|
||||
background-color: $light-200;
|
||||
}
|
||||
|
||||
.screen-header-primary {
|
||||
background-color: $primary-400;
|
||||
}
|
||||
|
||||
.large-screen-container {
|
||||
background-color: $white;
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
.large-screen-svg-light,
|
||||
.large-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.large-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
}
|
||||
|
||||
.medium-screen-container {
|
||||
flex-wrap: nowrap;
|
||||
max-width: inherit;
|
||||
height: 282px;
|
||||
}
|
||||
|
||||
.variation1-medium-screen {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
.medium-screen-svg-light,
|
||||
.medium-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
overflow: inherit;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.medium-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
}
|
||||
|
||||
.screen-polygon {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.small-screen-header-light,
|
||||
.small-screen-header-primary {
|
||||
background-color: $light-200;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.small-screen-header-primary {
|
||||
background-color: $primary-400;
|
||||
}
|
||||
|
||||
.extra-large-screen-top-stripe {
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 45%,
|
||||
$primary-700 45%,
|
||||
$primary-700 55%,
|
||||
$accent-a 55%,
|
||||
$accent-a 75%,
|
||||
$accent-a-light 75%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.medium-screen-top-stripe {
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 90%,
|
||||
$primary-700 90%,
|
||||
$primary-700 100%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.large-screen-top-stripe {
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 65%,
|
||||
$primary-700 65%,
|
||||
$primary-700 75%,
|
||||
$accent-a 75%,
|
||||
$accent-a 75%);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.small-screen-top-stripe {
|
||||
height: 0.25rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 20%,
|
||||
$brand 20%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
// Progressive profiling base component classes
|
||||
.medium-container {
|
||||
flex-wrap: nowrap;
|
||||
max-width: inherit;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.extra-extra-large-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
|
||||
width: 5.5rem;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.extra-large-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
|
||||
width: 5.5rem;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.medium-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
width: 7em;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.small-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.25rem;
|
||||
|
||||
width: 4em;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.extra-small-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.25rem;
|
||||
|
||||
width: 4em;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
// Non-Auth Screen Svg Lines
|
||||
.large-screen-svg-line {
|
||||
padding-top: 0.5rem;
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
width: 5.5rem;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.variation1-bar-color {
|
||||
stroke: $brand !important;
|
||||
}
|
||||
|
||||
.variation2-bar-color {
|
||||
stroke: $accent-a !important;
|
||||
}
|
||||
|
||||
.medium-screen-svg-line {
|
||||
padding-top: 0.5rem;
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
width: 7em;
|
||||
height: 115px;
|
||||
}
|
||||
|
||||
.small-screen-svg-line {
|
||||
padding-top: 0.5rem;
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.25rem;
|
||||
width: 4em;
|
||||
height: 72px;
|
||||
}
|
||||
.dicount-heading{
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.hover-text:hover {
|
||||
color: $black !important;
|
||||
|
||||
.hover-icon {
|
||||
color: $black !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hover-discount-icon:hover {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.large-heading {
|
||||
margin-left: 7px;
|
||||
color: $white;
|
||||
max-width: 24.5rem;
|
||||
line-height: 78px;
|
||||
font-size: 78px;
|
||||
}
|
||||
|
||||
.medium-heading {
|
||||
padding-left: 1rem;
|
||||
color: $white;
|
||||
max-width: 27rem;
|
||||
line-height: 60px;
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.small-heading {
|
||||
padding-left: 0.5rem;
|
||||
color: $white;
|
||||
line-height: 40px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 4.44rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.username-suggestion {
|
||||
padding: 1px 0.5rem;
|
||||
margin: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
.username-suggestion-label {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.yellow-border {
|
||||
border: 2px solid #F0CC00;
|
||||
}
|
||||
|
||||
.one-rem-font {
|
||||
font-size: 0.99rem;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
.institute-heading {
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
.logistration-button {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.logistration-button:hover{
|
||||
color: $gray-700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-item:active {
|
||||
background-color: #F2F0EF;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), 0px 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
line-height: 1.25rem;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
width: 464px;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
.email-error-alert {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.alert-close {
|
||||
float: right;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: $primary !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-warning-alert-link {
|
||||
color: $info-500 !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#forgot-password {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-back-icon {
|
||||
margin-top:2px;
|
||||
}
|
||||
|
||||
.icon-size {
|
||||
width: 2.3rem;
|
||||
}
|
||||
.has-floating-label {
|
||||
color: $gray-500;
|
||||
}
|
||||
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pgn__form-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 16px;
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
.form-field .form-control:not([value='']):not(:focus) ~
|
||||
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pgn__form-group {
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.form-text-size {
|
||||
margin-top: 0.188rem !important;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.progressive-profiling-support {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.discount-banner {
|
||||
background-color: #03C7E8;
|
||||
}
|
||||
|
||||
.dashed-border {
|
||||
border-style: dashed;
|
||||
border-width: thin;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.mw-500 {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 463px) {
|
||||
.reset-password-container {
|
||||
width: 420px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.tpa-skeleton {
|
||||
min-width: 464px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.welcome-page-content {
|
||||
padding-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction:column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dashed-border {
|
||||
border-style: dashed;
|
||||
border-width: thin;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) and (min-width: 768px) {
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction:column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.layout{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
.content {
|
||||
width: 50vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 464px) {
|
||||
.dropdown-container {
|
||||
width: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 500px) {
|
||||
.large-screen-svg-line, .large-heading {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 960px) {
|
||||
html {
|
||||
margin-right: calc(100% - 100vw);
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.variation2-text-alignment {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
// Smaller than Extra Small (Mobile Screens)
|
||||
@media (max-width: 464px) {
|
||||
.btn-social {
|
||||
min-width: 100%;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.welcome-page-content,
|
||||
.main-content {
|
||||
min-width: 100vw !important;
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.progressive-profiling-support {
|
||||
font-size: 0.688rem;
|
||||
font-weight: normal;
|
||||
line-height: 0.938rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: $light-200;
|
||||
}
|
||||
|
||||
.secondary-provider-link {
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
color: $primary-700
|
||||
}
|
||||
|
||||
.opt-checkbox {
|
||||
.pgn__form-label {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
margin-left: 3px;
|
||||
}
|
||||
.suggested-username {
|
||||
position: relative;
|
||||
margin-top: -8.7%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.suggested-username-close-button {
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
}
|
||||
.suggested-username-with-error {
|
||||
position: relative;
|
||||
margin-top: -13.7%;
|
||||
margin-bottom: 11%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.scroll-suggested-username {
|
||||
width: 21rem;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.pgn__form-control-decorator-trailing {
|
||||
right: 0 !important;
|
||||
}
|
||||
6
src/app.scss
Executable file
6
src/app.scss
Executable file
@@ -0,0 +1,6 @@
|
||||
@use "@edx/brand/paragon/fonts";
|
||||
@use "@edx/brand/paragon/variables";
|
||||
@use "@openedx/paragon/scss/core/core";
|
||||
@use "@edx/brand/paragon/overrides";
|
||||
|
||||
@use "sass/style";
|
||||
43
src/app.ts
Normal file
43
src/app.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { App } from '@openedx/frontend-base';
|
||||
import { appId } from './constants';
|
||||
import routes from './routes';
|
||||
import messages from './i18n';
|
||||
|
||||
const app: App = {
|
||||
appId,
|
||||
routes,
|
||||
messages,
|
||||
config: {
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: null,
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: null,
|
||||
BANNER_IMAGE_EXTRA_SMALL: '',
|
||||
BANNER_IMAGE_LARGE: '',
|
||||
BANNER_IMAGE_MEDIUM: '',
|
||||
BANNER_IMAGE_SMALL: '',
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: false,
|
||||
ENABLE_IMAGE_LAYOUT: false,
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: false,
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
INFO_EMAIL: '',
|
||||
LOGIN_ISSUE_SUPPORT_LINK: null,
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
MARKETING_SITE_BASE_URL: 'http://local.openedx.io',
|
||||
PASSWORD_RESET_SUPPORT_LINK: null,
|
||||
POST_REGISTRATION_REDIRECT_URL: '',
|
||||
PRIVACY_POLICY: null,
|
||||
SEARCH_CATALOG_URL: null,
|
||||
SESSION_COOKIE_DOMAIN: 'local.openedx.io',
|
||||
SHOW_REGISTRATION_LINKS: true,
|
||||
TOS_AND_HONOR_CODE: null,
|
||||
TOS_LINK: null,
|
||||
USER_RETENTION_COOKIE_NAME: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default app;
|
||||
@@ -1,97 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Col, Hyperlink, Image, Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthExtraLargeLayout = (props) => {
|
||||
const { intl, username, variant } = props;
|
||||
|
||||
return (
|
||||
<div className="container row p-0 m-0 large-screen-container">
|
||||
<div className="col-md-9 p-0 screen-header-light">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="min-vh-100 d-flex align-items-center">
|
||||
<div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<svg className={classNames(
|
||||
'ml-5 mt-5',
|
||||
{
|
||||
'extra-large-svg-line': variant === 'xl',
|
||||
'extra-extra-large-svg-line': variant === 'xxl',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="60" y1="0" x2="5" y2="220" />
|
||||
</svg>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<div className={classNames(
|
||||
'data-hj-suppress',
|
||||
{
|
||||
h3: variant === 'xl',
|
||||
h2: variant === 'xxl',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'text-primary',
|
||||
{
|
||||
'display-1': variant === 'xl',
|
||||
'display-2': variant === 'xxl',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<span className="text-accent-a">
|
||||
<br />
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3 p-0 screen-polygon">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="m1-n1 large-screen-svg-light"
|
||||
preserveAspectRatio="xMaxYMin meet"
|
||||
>
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuthExtraLargeLayout.defaultProps = {
|
||||
variant: 'xl',
|
||||
};
|
||||
|
||||
AuthExtraLargeLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
variant: PropTypes.oneOf(['xl', 'xxl']),
|
||||
};
|
||||
|
||||
export default injectIntl(AuthExtraLargeLayout);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
Col, Hyperlink, Image, Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthMediumLayout = (props) => {
|
||||
const { intl, username } = props;
|
||||
|
||||
return (
|
||||
<div className="container row p-0 mb-3 medium-container">
|
||||
<div className="col-md-10 p-0 screen-header-light">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center justify-content-center ml-6">
|
||||
<div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<svg className="medium-svg-line ml-5 mt-5">
|
||||
<line x1="60" y1="0" x2="5" y2="220" />
|
||||
</svg>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<h3 className="data-hj-suppress">
|
||||
{intl.formatMessage(
|
||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
||||
)}
|
||||
</h3>
|
||||
<div className="display-1 text-primary">
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<span className="text-accent-a">
|
||||
<br />
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 p-0 screen-polygon">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="medium-screen-svg-light"
|
||||
preserveAspectRatio="xMaxYMin meet"
|
||||
>
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuthMediumLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthMediumLayout);
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Col, Hyperlink, Image, Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthSmallLayout = (props) => {
|
||||
const { intl, username, variant } = props;
|
||||
|
||||
return (
|
||||
<div className="small-screen-header-light">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className={classNames('d-flex mt-3', { 'pl-6': variant === 'sm' })}>
|
||||
<div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<svg className={classNames(
|
||||
'mt-4\.5', // eslint-disable-line no-useless-escape
|
||||
{
|
||||
'extra-small-svg-line': variant === 'xs',
|
||||
'small-svg-line': variant === 'sm',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="60" y1="0" x2="5" y2="220" />
|
||||
</svg>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<h5 className="data-hj-suppress">
|
||||
{intl.formatMessage(
|
||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
||||
)}
|
||||
</h5>
|
||||
<h1>
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<br />
|
||||
<span className="text-accent-a">
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</span>
|
||||
</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuthSmallLayout.defaultProps = {
|
||||
variant: 'sm',
|
||||
};
|
||||
|
||||
AuthSmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
variant: PropTypes.oneOf(['sm', 'xs']),
|
||||
};
|
||||
|
||||
export default injectIntl(AuthSmallLayout);
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LargeLayout from './LargeLayout';
|
||||
import MediumLayout from './MediumLayout';
|
||||
import SmallLayout from './SmallLayout';
|
||||
|
||||
import AuthExtraLargeLayout from './AuthExtraLargeLayout';
|
||||
import AuthMediumLayout from './AuthMediumLayout';
|
||||
import AuthSmallLayout from './AuthSmallLayout';
|
||||
import DiscountExperimentBanner from './DiscountBanner';
|
||||
|
||||
const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => {
|
||||
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
|
||||
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const { experimentName } = window;
|
||||
|
||||
if (experimentName) {
|
||||
setOptimizelyExperimentName(experimentName);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRegistrationPage && optimizelyExperimentName === 'variation2' ? <DiscountExperimentBanner /> : null}
|
||||
<CookiePolicyBanner languageCode={getLocale()} />
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraExtraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
</MediaQuery>
|
||||
|
||||
<div className={classNames('layout', { authenticated: authenticatedUser })}>
|
||||
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth}>
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthSmallLayout variant="xs" username={authenticatedUser.username} /> : (
|
||||
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth}>
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthSmallLayout username={authenticatedUser.username} /> : (
|
||||
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.medium.maxWidth}>
|
||||
<div className="w-100 medium-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : (
|
||||
<MediumLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.large.minWidth} maxWidth={breakpoints.large.maxWidth}>
|
||||
<div className="w-100 large-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : (
|
||||
<MediumLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
|
||||
{authenticatedUser ? <AuthExtraLargeLayout username={authenticatedUser.username} /> : (
|
||||
<LargeLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraExtraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
||||
{authenticatedUser ? <AuthExtraLargeLayout variant="xxl" username={authenticatedUser.username} /> : (
|
||||
<LargeLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
|
||||
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BaseComponent.defaultProps = {
|
||||
isRegistrationPage: false,
|
||||
showWelcomeBanner: false,
|
||||
};
|
||||
|
||||
BaseComponent.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaseComponent;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { Toast, PageBanner } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
|
||||
const DiscountExperimentBanner = (props) => {
|
||||
const { intl } = props;
|
||||
const [show, setShow] = useState(true);
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
const getDiscountText = () => (
|
||||
<strong>
|
||||
15% <FormattedMessage
|
||||
id="top.discount.message.15.off"
|
||||
defaultMessage="off"
|
||||
description="Text used with discounts e.g. 15% off"
|
||||
/>
|
||||
</strong>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<PageBanner
|
||||
show={show}
|
||||
dismissible
|
||||
onDismiss={() => { setShow(false); }}
|
||||
>
|
||||
<span className="text-primary-700 small variation2-text-alignment">
|
||||
<span className="mr-3">
|
||||
<FormattedMessage
|
||||
id="top.discount.message.body"
|
||||
defaultMessage="Get {discount} your first verified certificate* with code"
|
||||
description="Message body for edX discount"
|
||||
values={{
|
||||
discount: getDiscountText(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="hover-text dashed-border p-1 d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="font-weight-bold ">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-dark-200 copyIcon ml-2 hover-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</PageBanner>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DiscountExperimentBanner.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
};
|
||||
export default injectIntl(DiscountExperimentBanner);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
|
||||
import LargeScreenLeftLayout from './LargeLeftLayout';
|
||||
|
||||
const LargeLayout = ({ experimentName, isRegistrationPage }) => (
|
||||
<div className="container row p-0 m-0 large-screen-container">
|
||||
<div className="col-md-9 p-0 screen-header-primary">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<LargeScreenLeftLayout experimentName={experimentName} isRegistrationPage={isRegistrationPage} />
|
||||
</div>
|
||||
<div className="col-md-3 p-0 screen-polygon">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="ml-n1 large-screen-svg-primary"
|
||||
preserveAspectRatio="xMaxYMin meet"
|
||||
>
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
LargeLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
};
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Toast } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import SideDiscountBanner from './SideDiscountBanner';
|
||||
|
||||
const LargeLeftLayout = (props) => {
|
||||
const { intl, isRegistrationPage, experimentName } = props;
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<div className="min-vh-100 d-flex justify-content-left align-items-center">
|
||||
<div className="d-flex pr-0 mt-lg-n2">
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<svg className={classNames(
|
||||
'large-screen-svg-line',
|
||||
{
|
||||
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="50" y1="0" x2="10" y2="215" />
|
||||
</svg>
|
||||
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
<br />
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{experimentName === 'variation1' && isRegistrationPage ? (
|
||||
<span className="text-light-300 dicount-heading">
|
||||
<span className="lead mr-3">
|
||||
<SideDiscountBanner />
|
||||
</span>
|
||||
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LargeLeftLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
LargeLeftLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
};
|
||||
|
||||
export default injectIntl(LargeLeftLayout);
|
||||
@@ -1,102 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Image, Toast } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import messages from './messages';
|
||||
import SideDiscountBanner from './SideDiscountBanner';
|
||||
|
||||
const MediumLayout = (props) => {
|
||||
const { intl, isRegistrationPage, experimentName } = props;
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
'container row p-0 mb-3 medium-screen-container',
|
||||
{
|
||||
'variation1-medium-screen': experimentName === 'variation1' && isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<div className="col-md-10 p-0 screen-header-primary">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="row mt-4 justify-content-center">
|
||||
<svg className={classNames(
|
||||
'medium-screen-svg-line pl-5',
|
||||
{
|
||||
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="50" y1="0" x2="10" y2="215" />
|
||||
</svg>
|
||||
<div className="pb-4">
|
||||
<h1 className="medium-heading">
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
<br />
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{experimentName === 'variation1' && isRegistrationPage ? (
|
||||
<div className="text-light-300 pl-3">
|
||||
<SideDiscountBanner />
|
||||
<span className="dashed-border h5 text-white">
|
||||
<span id="edx-welcome" className="edx-welcome">EDXWELCOME </span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div className="col-md-2 p-0 screen-polygon">
|
||||
<svg width="100%" height="100%" className="medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
MediumLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
};
|
||||
|
||||
export default injectIntl(MediumLayout);
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function SideDiscountBanner() {
|
||||
const getDiscountText = () => (
|
||||
<span className="text-accent-a h3">
|
||||
15% <FormattedMessage
|
||||
id="side.discount.message.15.off"
|
||||
defaultMessage="off"
|
||||
description="Text used with discounts e.g. 15% off"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
const getCerificateMsg = () => (
|
||||
<span className="dicount-heading">
|
||||
<FormattedMessage
|
||||
id="certificate.message"
|
||||
defaultMessage="certificate* with code"
|
||||
description="Text with certificate"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span className="mr-1.5">
|
||||
<FormattedMessage
|
||||
id="side.discount.message.body"
|
||||
defaultMessage="Get {discountText} your first verified {lineBreak} {certificateMsg}"
|
||||
description="Message body for edX discount"
|
||||
values={{
|
||||
discountText: getDiscountText(),
|
||||
lineBreak: <br />,
|
||||
certificateMsg: getCerificateMsg(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Image, Toast } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import messages from './messages';
|
||||
import SideDiscountBanner from './SideDiscountBanner';
|
||||
|
||||
const SmallLayout = (props) => {
|
||||
const { intl, isRegistrationPage, experimentName } = props;
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="small-screen-header-primary">
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex mt-3">
|
||||
<svg className={classNames(
|
||||
'small-screen-svg-line',
|
||||
{
|
||||
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="55" y1="0" x2="40" y2="65" />
|
||||
</svg>
|
||||
<div className="pb-3">
|
||||
<h1 className="small-heading">
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<br />
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{(experimentName === 'variation1' && isRegistrationPage) ? (
|
||||
<div className="small text-light-300 pl-2">
|
||||
<SideDiscountBanner />
|
||||
<span className="dashed-border h6 text-white d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="edx-welcome mr-1">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
SmallLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
|
||||
};
|
||||
|
||||
export default injectIntl(SmallLayout);
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './BaseComponent';
|
||||
@@ -1,37 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'start.learning': {
|
||||
id: 'start.learning',
|
||||
defaultMessage: 'Start learning',
|
||||
description: 'Header text for logistration MFE pages',
|
||||
},
|
||||
'with.site.name': {
|
||||
id: 'with.site.name',
|
||||
defaultMessage: 'with {siteName}',
|
||||
description: 'Header text with site name for logistration MFE pages',
|
||||
},
|
||||
'code.copied': {
|
||||
id: 'code.copied',
|
||||
defaultMessage: 'Code copied',
|
||||
description: 'part of 15% discount code copied',
|
||||
},
|
||||
// authenticated user base component text
|
||||
'complete.your.profile.1': {
|
||||
id: 'complete.your.profile.1',
|
||||
defaultMessage: 'Complete',
|
||||
description: 'part of text "complete your profile"',
|
||||
},
|
||||
'complete.your.profile.2': {
|
||||
id: 'complete.your.profile.2',
|
||||
defaultMessage: 'your profile',
|
||||
description: 'part of text "complete your profile"',
|
||||
},
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {username}!',
|
||||
description: 'Welcome message that appears on progressive profile page',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LargeLayout from '../LargeLayout';
|
||||
import MediumLayout from '../MediumLayout';
|
||||
import SmallLayout from '../SmallLayout';
|
||||
|
||||
describe('ScreenLayout', () => {
|
||||
it('should display the form, pass as a child in SmallScreenLayout', () => {
|
||||
const smallScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<SmallLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(smallScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the form, pass as a child in MediumScreenLayout', () => {
|
||||
const mediumScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<MediumLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(mediumScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the form, pass as a child in LargeScreenLayout', () => {
|
||||
const largeScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<LargeLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(largeScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||
|
||||
describe('Default Layout tests', () => {
|
||||
it('should display the form passed as a child in SmallScreenLayout', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultSmallLayout />
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in MediumScreenLayout', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultMediumLayout />
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in LargeScreenLayout', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultLargeLayout />
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
});
|
||||
42
src/base-container/components/default-layout/LargeLayout.jsx
Normal file
42
src/base-container/components/default-layout/LargeLayout.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-9 bg-primary-400">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="min-vh-100 d-flex align-items-center">
|
||||
<div className={classNames({ 'large-yellow-line mr-n4.5': getSiteConfig().siteName === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-2 text-white mw-xs',
|
||||
{ 'ml-6': getSiteConfig().siteName !== 'edX' },
|
||||
)}
|
||||
>
|
||||
{formatMessage(messages['start.learning'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3 bg-white p-0">
|
||||
<svg className="ml-n1 w-100 h-100 large-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-100 medium-screen-top-stripe" />
|
||||
<div className="w-100 p-0 mb-3 d-flex">
|
||||
<div className="col-md-10 bg-primary-400">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getSiteConfig().siteName} className="logo" src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center justify-content-center mb-4 ">
|
||||
<div className={classNames({ 'mt-1 medium-yellow-line': getSiteConfig().siteName === 'edX' })} />
|
||||
<div>
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
|
||||
{ 'ml-4.5': getSiteConfig().siteName !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="w-100 h-100 medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
37
src/base-container/components/default-layout/SmallLayout.jsx
Normal file
37
src/base-container/components/default-layout/SmallLayout.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<span className="bg-primary-400 w-100">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<div>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center m-3.5">
|
||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getSiteConfig().siteName === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'text-white mt-3.5 mb-3.5',
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
3
src/base-container/components/default-layout/index.jsx
Normal file
3
src/base-container/components/default-layout/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as DefaultLargeLayout } from './LargeLayout';
|
||||
export { default as DefaultMediumLayout } from './MediumLayout';
|
||||
export { default as DefaultSmallLayout } from './SmallLayout';
|
||||
16
src/base-container/components/default-layout/messages.js
Normal file
16
src/base-container/components/default-layout/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
'start.learning': {
|
||||
id: 'start.learning',
|
||||
defaultMessage: 'Start learning',
|
||||
description: 'Header text for logistration MFE pages',
|
||||
},
|
||||
'with.site.name': {
|
||||
id: 'with.site.name',
|
||||
defaultMessage: 'with {siteName}',
|
||||
description: 'Header text with site name for logistration MFE pages',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ExtraSmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<span
|
||||
className="w-100 bg-primary-500 banner__image extra-small-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="ml-4.5 mr-1 pb-3.5 pt-3.5">
|
||||
<h1 className="banner__heading">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||
</span>
|
||||
<span className="text-warning-300">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtraSmallLayout;
|
||||
32
src/base-container/components/image-layout/LargeLayout.jsx
Normal file
32
src/base-container/components/image-layout/LargeLayout.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-50 bg-primary-500 banner__image large-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_LARGE})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="min-vh-100 p-5 d-flex align-items-end">
|
||||
<h1 className="display-2 mw-sm mb-3 d-flex flex-column flex-shrink-0 justify-content-center">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}
|
||||
</span>
|
||||
<span className="text-warning-300">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
32
src/base-container/components/image-layout/MediumLayout.jsx
Normal file
32
src/base-container/components/image-layout/MediumLayout.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_MEDIUM})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="ml-5 pb-4 pt-4">
|
||||
<h1 className="display-2 banner__heading">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||
</span>
|
||||
<span className="text-warning-300 d-inline-block">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
31
src/base-container/components/image-layout/SmallLayout.jsx
Normal file
31
src/base-container/components/image-layout/SmallLayout.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<span
|
||||
className="w-100 bg-primary-500 banner__image small-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_SMALL})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="ml-5 mr-1 pb-3.5 pt-3.5">
|
||||
<h1 className="display-2">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||
</span>
|
||||
<span className="text-warning-300">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
4
src/base-container/components/image-layout/index.jsx
Normal file
4
src/base-container/components/image-layout/index.jsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as ImageLargeLayout } from './LargeLayout';
|
||||
export { default as ImageMediumLayout } from './MediumLayout';
|
||||
export { default as ImageSmallLayout } from './SmallLayout';
|
||||
export { default as ImageExtraSmallLayout } from './ExtraSmallLayout';
|
||||
37
src/base-container/components/image-layout/index.scss
Normal file
37
src/base-container/components/image-layout/index.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
.company-logo {
|
||||
width: 71px;
|
||||
margin-top: 2rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.company-logo {
|
||||
width: 44.67px;
|
||||
margin-top: 1.25rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.banner__image {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border:none;
|
||||
}
|
||||
|
||||
@media (min-width: 464px) and (max-width: 575.98px) {
|
||||
.banner__heading {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
line-height: 60px;
|
||||
letter-spacing: -1.2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 800px) {
|
||||
.banner__heading {
|
||||
font-size: 60px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 60px !important;
|
||||
letter-spacing: -2px !important;
|
||||
}
|
||||
}
|
||||
16
src/base-container/components/image-layout/messages.js
Normal file
16
src/base-container/components/image-layout/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
'your.career.turning.point': {
|
||||
id: 'your.career.turning.point',
|
||||
defaultMessage: 'Your career turning point',
|
||||
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
|
||||
},
|
||||
'is.here': {
|
||||
id: 'is.here',
|
||||
defaultMessage: 'is here.',
|
||||
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-10 bg-light-200 p-0">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<div className="min-vh-100 d-flex align-items-center">
|
||||
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
|
||||
<div>
|
||||
<h1 className="welcome-to-platform data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||
</h1>
|
||||
<h2 className="complete-your-profile">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="m1-n1 w-100 h-100 large-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-100 medium-screen-top-stripe" />
|
||||
<div className="w-100 p-0 mb-3 d-flex">
|
||||
<div className="col-md-10 bg-light-200">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center justify-content-center mb-4 ml-5">
|
||||
<div className="medium-yellow-line mt-5 mr-n2" />
|
||||
<div>
|
||||
<h1 className="h3 data-hj-suppress mw-320">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||
</h1>
|
||||
<h2 className="display-1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="w-100 h-100 medium-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="min-vw-100 bg-light-200">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center m-3.5">
|
||||
<div className="small-yellow-line mt-4.5" />
|
||||
<div>
|
||||
<h1 className="h5 data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||
</h1>
|
||||
<h2 className="h1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as AuthLargeLayout } from './LargeLayout';
|
||||
export { default as AuthMediumLayout } from './MediumLayout';
|
||||
export { default as AuthSmallLayout } from './SmallLayout';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {fullName}!',
|
||||
description: 'Welcome message that appears on progressive profile page',
|
||||
},
|
||||
'complete.your.profile.1': {
|
||||
id: 'complete.your.profile.1',
|
||||
defaultMessage: 'Complete',
|
||||
description: 'part of text "complete your profile"',
|
||||
},
|
||||
'complete.your.profile.2': {
|
||||
id: 'complete.your.profile.2',
|
||||
defaultMessage: 'your profile',
|
||||
description: 'part of text "complete your profile"',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
70
src/base-container/index.jsx
Normal file
70
src/base-container/index.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useAppConfig } from '@openedx/frontend-base';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MediaQuery from 'react-responsive';
|
||||
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './components/default-layout';
|
||||
import {
|
||||
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
|
||||
} from './components/image-layout';
|
||||
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||
|
||||
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
const enableImageLayout = useAppConfig().ENABLE_IMAGE_LAYOUT;
|
||||
|
||||
if (enableImageLayout) {
|
||||
return (
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageExtraSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <ImageMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <ImageLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <DefaultSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <DefaultMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <DefaultLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BaseContainer.defaultProps = {
|
||||
showWelcomeBanner: false,
|
||||
fullName: null,
|
||||
};
|
||||
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
fullName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BaseContainer;
|
||||
44
src/base-container/tests/BaseContainer.test.jsx
Normal file
44
src/base-container/tests/BaseContainer.test.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { IntlProvider, mergeAppConfig } from '@openedx/frontend-base';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
|
||||
import BaseContainer from '../index';
|
||||
import { appId } from '../../constants';
|
||||
|
||||
const LargeScreen = {
|
||||
wrappingComponent: ResponsiveContext.Provider,
|
||||
wrappingComponentProps: { value: { width: 1200 } },
|
||||
};
|
||||
|
||||
describe('Base component tests', () => {
|
||||
it('should show default layout', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer>
|
||||
<div>Test Content</div>
|
||||
</BaseContainer>
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeNull();
|
||||
expect(container.querySelector('.large-screen-svg-primary')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
||||
mergeAppConfig(appId, {
|
||||
ENABLE_IMAGE_LAYOUT: true,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer showWelcomeBanner={false}>
|
||||
<div>Test Content</div>
|
||||
</BaseContainer>
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeDefined();
|
||||
});
|
||||
});
|
||||
26
src/common-components/EmbeddedRegistrationRoute.jsx
Normal file
26
src/common-components/EmbeddedRegistrationRoute.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { PAGE_NOT_FOUND } from '../data/constants';
|
||||
import { isHostAvailableInQueryParams } from '../data/utils';
|
||||
|
||||
/**
|
||||
* This wrapper redirects the requester to embedded register page only if host
|
||||
* query param is present.
|
||||
*/
|
||||
const EmbeddedRegistrationRoute = ({ children }) => {
|
||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||
|
||||
// Show registration page for embedded experience even if the user is authenticated
|
||||
if (registrationEmbedded) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return <Navigate to={PAGE_NOT_FOUND} replace />;
|
||||
};
|
||||
|
||||
EmbeddedRegistrationRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default EmbeddedRegistrationRoute;
|
||||
@@ -1,25 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {
|
||||
Form, Button,
|
||||
} from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import {
|
||||
Button, Form,
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
* */
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const tpaProvider = props.provider;
|
||||
const hideRegistrationLink = useAppConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|
||||
|| useAppConfig().SHOW_REGISTRATION_LINKS === false;
|
||||
|
||||
const handleSubmit = (e, url) => {
|
||||
e.preventDefault();
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
window.location.href = getSiteConfig().lmsBaseUrl + url;
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
@@ -33,7 +35,7 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-450">
|
||||
<Form className="m-0">
|
||||
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<p>{formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<Button
|
||||
id={tpaProvider.id}
|
||||
key={tpaProvider.id}
|
||||
@@ -44,30 +46,35 @@ const EnterpriseSSO = (props) => {
|
||||
>
|
||||
{tpaProvider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||
<span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||
{SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? (
|
||||
<FontAwesomeIcon icon={['fab', tpaProvider.iconClass]} />)
|
||||
: (
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
</div>
|
||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||
<span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="mb-4" />
|
||||
<Button
|
||||
type="submit"
|
||||
id="other-ways-to-sign-in"
|
||||
variant="outline-primary"
|
||||
state="Complete"
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
{hideRegistrationLink
|
||||
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -98,7 +105,6 @@ EnterpriseSSO.propTypes = {
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EnterpriseSSO);
|
||||
export default EnterpriseSSO;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Form, TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormGroup = (props) => {
|
||||
@@ -27,8 +27,9 @@ const FormGroup = (props) => {
|
||||
readOnly={props.readOnly}
|
||||
type={props.type}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
className="form-field"
|
||||
className="form-group__form-field"
|
||||
autoComplete={props.autoComplete}
|
||||
spellCheck={props.spellCheck}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
onFocus={handleFocus}
|
||||
@@ -36,7 +37,6 @@ const FormGroup = (props) => {
|
||||
onClick={handleClick}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
|
||||
trailingElement={props.trailingElement}
|
||||
floatingLabel={props.floatingLabel}
|
||||
>
|
||||
@@ -64,41 +64,43 @@ const FormGroup = (props) => {
|
||||
|
||||
FormGroup.defaultProps = {
|
||||
as: 'input',
|
||||
errorMessage: '',
|
||||
borderClass: '',
|
||||
autoComplete: null,
|
||||
readOnly: false,
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
handleFocus: null,
|
||||
handleClick: null,
|
||||
helpText: [],
|
||||
options: null,
|
||||
trailingElement: null,
|
||||
type: 'text',
|
||||
borderClass: '',
|
||||
children: null,
|
||||
className: '',
|
||||
errorMessage: '',
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
handleClick: null,
|
||||
handleFocus: null,
|
||||
helpText: [],
|
||||
options: null,
|
||||
readOnly: false,
|
||||
spellCheck: null,
|
||||
trailingElement: null,
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
FormGroup.propTypes = {
|
||||
as: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
borderClass: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
borderClass: PropTypes.string,
|
||||
children: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
floatingLabel: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleClick: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
helpText: PropTypes.arrayOf(PropTypes.string),
|
||||
name: PropTypes.string.isRequired,
|
||||
options: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
spellCheck: PropTypes.string,
|
||||
trailingElement: PropTypes.element,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
children: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormGroup;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Button, Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Institution login button
|
||||
* */
|
||||
export const RenderInstitutionButton = props => {
|
||||
const { onSubmitHandler, buttonTitle } = props;
|
||||
|
||||
@@ -22,10 +24,13 @@ export const RenderInstitutionButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component renders the page list of available institutions for login
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const lmsBaseUrl = getSiteConfig().lmsBaseUrl;
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl,
|
||||
secondaryProviders,
|
||||
headingTitle,
|
||||
} = props;
|
||||
@@ -34,11 +39,11 @@ const InstitutionLogistration = props => {
|
||||
<>
|
||||
<div className="d-flex justify-content-left mb-4 mt-2">
|
||||
<div className="flex-column">
|
||||
<h4 className="mb-2 font-weight-bold institute-heading">
|
||||
<h4 className="mb-2 font-weight-bold institutions__heading">
|
||||
{headingTitle}
|
||||
</h4>
|
||||
<p className="mb-2">
|
||||
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
{formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,7 +54,7 @@ const InstitutionLogistration = props => {
|
||||
<tr key={provider} className="pgn__data-table-row">
|
||||
<td>
|
||||
<Hyperlink
|
||||
className="btn nav-item p-0 mb-1 secondary-provider-link"
|
||||
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
||||
destination={lmsBaseUrl + provider.loginUrl}
|
||||
>
|
||||
{provider.name}
|
||||
@@ -87,7 +92,6 @@ RenderInstitutionButton.defaultProps = {
|
||||
|
||||
InstitutionLogistration.propTypes = {
|
||||
...LogistrationProps,
|
||||
intl: intlShape.isRequired,
|
||||
headingTitle: PropTypes.string,
|
||||
};
|
||||
InstitutionLogistration.defaultProps = {
|
||||
@@ -95,4 +99,4 @@ InstitutionLogistration.defaultProps = {
|
||||
headingTitle: '',
|
||||
};
|
||||
|
||||
export default injectIntl(InstitutionLogistration);
|
||||
export default InstitutionLogistration;
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams, getTpaHint } from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { RegistrationPage } from '../register';
|
||||
import BaseComponent from '../base-component';
|
||||
|
||||
const Logistration = (props) => {
|
||||
const { intl, selectedPage } = props;
|
||||
const tpa = getTpaHint();
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const authService = getAuthService();
|
||||
if (authService) {
|
||||
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
|
||||
}
|
||||
});
|
||||
|
||||
const handleInstitutionLogin = (e) => {
|
||||
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
||||
if (typeof e === 'string') {
|
||||
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
|
||||
} else {
|
||||
sendPageEvent('login_and_registration', e.target.dataset.eventName);
|
||||
}
|
||||
|
||||
setInstitutionLogin(!institutionLogin);
|
||||
};
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
const tabTitle = (
|
||||
<div className="d-flex">
|
||||
<Icon src={ChevronLeft} className="left-icon" />
|
||||
<span className="ml-2">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['logistration.sign.in'])
|
||||
: intl.formatMessage(messages['logistration.register'])}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseComponent isRegistrationPage={selectedPage === REGISTER_PAGE}>
|
||||
<div>
|
||||
{institutionLogin
|
||||
? (
|
||||
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{!tpa && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
|
||||
</div>
|
||||
</div>
|
||||
</BaseComponent>
|
||||
);
|
||||
};
|
||||
|
||||
Logistration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
selectedPage: PropTypes.string,
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
selectedPage: REGISTER_PAGE,
|
||||
};
|
||||
|
||||
export default injectIntl(Logistration);
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage } from '@openedx/frontend-base';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted mw-32em">
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted mw-32em">
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
|
||||
@@ -1,53 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import {
|
||||
Form, IconButton, useToggle, Tooltip, OverlayTrigger, Icon,
|
||||
} from '@edx/paragon';
|
||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Check, Remove, Visibility, VisibilityOff,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
|
||||
import { validatePasswordField } from '../register/data/utils';
|
||||
import messages from './messages';
|
||||
|
||||
const PasswordField = (props) => {
|
||||
const { formatMessage } = props.intl;
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const handleBlur = (e) => {
|
||||
if (props.handleBlur) { props.handleBlur(e); }
|
||||
const { name, value } = e.target;
|
||||
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
||||
return; // Do not run validations on password icon click
|
||||
}
|
||||
|
||||
let passwordValue = value;
|
||||
if (name === 'passwordIcon') {
|
||||
// To validate actual password value when onBlur is triggered by focusing out the password icon
|
||||
passwordValue = props.value;
|
||||
}
|
||||
|
||||
if (props.handleBlur) {
|
||||
props.handleBlur({
|
||||
target: {
|
||||
name: props.name,
|
||||
value: passwordValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setShowTooltip(props.showRequirements && false);
|
||||
if (props.handleErrorChange) { // If rendering from register page
|
||||
const fieldError = validatePasswordField(passwordValue, formatMessage);
|
||||
if (fieldError) {
|
||||
props.handleErrorChange('password', fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ password: passwordValue }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
if (e.target?.name === 'passwordIcon') {
|
||||
return; // Do not clear error on password icon focus
|
||||
}
|
||||
|
||||
if (props.handleFocus) {
|
||||
props.handleFocus(e);
|
||||
}
|
||||
if (props.handleErrorChange) {
|
||||
props.handleErrorChange('password', '');
|
||||
dispatch(clearRegistrationBackendError('password'));
|
||||
}
|
||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||
};
|
||||
|
||||
const HideButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
<IconButton
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
name="passwordIcon"
|
||||
src={VisibilityOff}
|
||||
iconAs={Icon}
|
||||
onClick={setHiddenTrue}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt={formatMessage(messages['hide.password'])}
|
||||
/>
|
||||
);
|
||||
|
||||
const ShowButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
<IconButton
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
name="passwordIcon"
|
||||
src={Visibility}
|
||||
iconAs={Icon}
|
||||
onClick={setHiddenFalse}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt={formatMessage(messages['show.password'])}
|
||||
/>
|
||||
);
|
||||
|
||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||
const tooltip = (
|
||||
<Tooltip id={`password-requirement-${placement}`}>
|
||||
<span id="letter-check" className="d-flex position-relative align-content-start">
|
||||
<span id="letter-check" className="d-flex align-items-center">
|
||||
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
|
||||
{formatMessage(messages['one.letter'])}
|
||||
</span>
|
||||
<span id="number-check" className="d-flex position-relative align-content-start">
|
||||
<span id="number-check" className="d-flex align-items-center">
|
||||
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
|
||||
{formatMessage(messages['one.number'])}
|
||||
</span>
|
||||
<span id="characters-check" className="d-flex position-relative align-content-start">
|
||||
<span id="characters-check" className="d-flex align-items-center">
|
||||
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
|
||||
{formatMessage(messages['eight.characters'])}
|
||||
</span>
|
||||
@@ -59,10 +121,11 @@ const PasswordField = (props) => {
|
||||
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
||||
<Form.Control
|
||||
as="input"
|
||||
className="form-field"
|
||||
className="form-group__form-field"
|
||||
type={isPasswordHidden ? 'password' : 'text'}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
autoComplete={props.autoComplete}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@@ -75,7 +138,7 @@ const PasswordField = (props) => {
|
||||
{props.errorMessage !== '' && (
|
||||
<Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">
|
||||
{props.errorMessage}
|
||||
<span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>
|
||||
{props.showScreenReaderText && <span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
@@ -88,7 +151,10 @@ PasswordField.defaultProps = {
|
||||
handleBlur: null,
|
||||
handleFocus: null,
|
||||
handleChange: () => {},
|
||||
handleErrorChange: null,
|
||||
showRequirements: true,
|
||||
showScreenReaderText: true,
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
PasswordField.propTypes = {
|
||||
@@ -98,10 +164,12 @@ PasswordField.propTypes = {
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
handleErrorChange: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
showScreenReaderText: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(PasswordField);
|
||||
export default PasswordField;
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig, getSiteConfig } from '@openedx/frontend-base';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { WELCOME_PAGE } from '../data/constants';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
function RedirectLogistration(props) {
|
||||
const RedirectLogistration = (props) => {
|
||||
const {
|
||||
finishAuthUrl, redirectUrl, redirectToWelcomePage, success,
|
||||
authenticatedUser,
|
||||
finishAuthUrl,
|
||||
redirectUrl,
|
||||
redirectToProgressiveProfilingPage,
|
||||
success,
|
||||
optionalFields,
|
||||
educationLevel,
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -20,36 +28,67 @@ function RedirectLogistration(props) {
|
||||
// Note: For multiple enterprise use case, we need to make sure that user first visits the
|
||||
// enterprise selection page and then complete the auth workflow
|
||||
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
|
||||
finalRedirectUrl = getConfig().LMS_BASE_URL + finishAuthUrl;
|
||||
finalRedirectUrl = getSiteConfig().lmsBaseUrl + finishAuthUrl;
|
||||
} else {
|
||||
finalRedirectUrl = redirectUrl;
|
||||
}
|
||||
|
||||
if (redirectToWelcomePage) {
|
||||
setCookie('van-504-returning-user', true);
|
||||
// use this component to redirect WelcomePage after successful registration
|
||||
// return <Redirect to={WELCOME_PAGE} />;
|
||||
// Redirect to Progressive Profiling after successful registration
|
||||
if (redirectToProgressiveProfilingPage) {
|
||||
// TODO: Do we still need this cookie?
|
||||
setCookie('van-504-returning-user', true, useAppConfig().SESSION_COOKIE_DOMAIN);
|
||||
|
||||
if (registrationEmbedded) {
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: useAppConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
}, host);
|
||||
return null;
|
||||
}
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return <Redirect to={{ pathname: WELCOME_PAGE, state: { registrationResult } }} />;
|
||||
return (
|
||||
<Navigate
|
||||
to={AUTHN_PROGRESSIVE_PROFILING}
|
||||
state={{
|
||||
registrationResult,
|
||||
optionalFields,
|
||||
authenticatedUser,
|
||||
}}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
RedirectLogistration.defaultProps = {
|
||||
authenticatedUser: {},
|
||||
educationLevel: null,
|
||||
finishAuthUrl: null,
|
||||
success: false,
|
||||
redirectUrl: '',
|
||||
redirectToWelcomePage: false,
|
||||
redirectToProgressiveProfilingPage: false,
|
||||
optionalFields: {},
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
authenticatedUser: PropTypes.shape({}),
|
||||
educationLevel: PropTypes.string,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
redirectUrl: PropTypes.string,
|
||||
redirectToWelcomePage: PropTypes.bool,
|
||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
function SocialAuthProviders(props) {
|
||||
const { intl, referrer, socialAuthProviders } = props;
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { referrer, socialAuthProviders } = props;
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = e.currentTarget.dataset.providerUrl;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
window.location.href = getSiteConfig().lmsBaseUrl + url;
|
||||
}
|
||||
|
||||
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||
@@ -30,29 +29,30 @@ function SocialAuthProviders(props) {
|
||||
>
|
||||
{provider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||
<img className="btn-tpa__image-icon" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||
{SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
|
||||
<FontAwesomeIcon icon={['fab', provider.iconClass]} />)
|
||||
: (
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
||||
<span className="sr-only">
|
||||
{referrer === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
</span>
|
||||
</button>
|
||||
));
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{socialAuth}</>;
|
||||
}
|
||||
};
|
||||
|
||||
SocialAuthProviders.defaultProps = {
|
||||
referrer: LOGIN_PAGE,
|
||||
@@ -60,7 +60,6 @@ SocialAuthProviders.defaultProps = {
|
||||
};
|
||||
|
||||
SocialAuthProviders.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
@@ -69,7 +68,8 @@ SocialAuthProviders.propTypes = {
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
skipRegistrationForm: PropTypes.bool,
|
||||
})),
|
||||
};
|
||||
|
||||
export default injectIntl(SocialAuthProviders);
|
||||
export default SocialAuthProviders;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// If the leaving child has focus, take control and redirect it
|
||||
if (htmlNode.contains(document.activeElement)) {
|
||||
// Get the newly entering sibling.
|
||||
// It's the previousSibling, but not for any explicit reason. So checking for both.
|
||||
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
|
||||
|
||||
// There's no replacement, do nothing.
|
||||
if (!enteringChild) return; // eslint-disable-line curly
|
||||
|
||||
// Get all the focusable elements in the entering child and focus the first one
|
||||
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusableElements.length) {
|
||||
focusableElements[0].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
return getContent(cases[caseKey]);
|
||||
}
|
||||
return React.cloneElement(cases[caseKey], { key: caseKey });
|
||||
} else if (cases.default) { // eslint-disable-line no-else-return
|
||||
if (typeof cases.default === 'string') {
|
||||
return getContent(cases.default);
|
||||
}
|
||||
React.cloneElement(cases.default, { key: 'default' });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionReplace
|
||||
className={className}
|
||||
onChildExit={onChildExit}
|
||||
>
|
||||
{getContent(expression)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
cases: PropTypes.objectOf(PropTypes.node).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
SwitchContent.defaultProps = {
|
||||
expression: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
export default SwitchContent;
|
||||
123
src/common-components/ThirdPartyAuth.jsx
Normal file
123
src/common-components/ThirdPartyAuth.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import {
|
||||
Hyperlink, Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
import {
|
||||
RenderInstitutionButton,
|
||||
SocialAuthProviders,
|
||||
} from './index';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
* */
|
||||
const ThirdPartyAuth = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
providers,
|
||||
secondaryProviders,
|
||||
currentProvider,
|
||||
handleInstitutionLogin,
|
||||
thirdPartyAuthApiStatus,
|
||||
isLoginPage,
|
||||
} = props;
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = useAppConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
const enterpriseLoginURL = getSiteConfig().lmsBaseUrl + ENTERPRISE_LOGIN_URL;
|
||||
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{isLoginPage
|
||||
? formatMessage(messages['login.other.options.heading'])
|
||||
: formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
{(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && (
|
||||
<Hyperlink
|
||||
className={classNames(
|
||||
'btn btn-link btn-sm text-body p-0',
|
||||
{ 'mb-0': thirdPartyAuthApiStatus === PENDING_STATE },
|
||||
{ 'mb-4': thirdPartyAuthApiStatus !== PENDING_STATE },
|
||||
)}
|
||||
destination={enterpriseLoginURL}
|
||||
>
|
||||
<Icon src={Institution} className="institute-icon" />
|
||||
{formatMessage(messages['enterprise.login.btn.text'])}
|
||||
</Hyperlink>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
|
||||
<div className="mt-4">
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={handleInstitutionLogin}
|
||||
buttonTitle={formatMessage(messages['institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders
|
||||
socialAuthProviders={providers}
|
||||
referrer={isLoginPage ? LOGIN_PAGE : REGISTER_PAGE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuth.defaultProps = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
isLoginPage: false,
|
||||
};
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
currentProvider: PropTypes.string,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
providers: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
secondaryProviders: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
isLoginPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ThirdPartyAuth;
|
||||
@@ -1,42 +1,49 @@
|
||||
import React from 'react';
|
||||
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { currentProvider, intl, referrer } = props;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const { formatMessage } = useIntl();
|
||||
const { currentProvider, referrer } = props;
|
||||
const platformName = getSiteConfig().siteName;
|
||||
let message;
|
||||
|
||||
if (referrer === LOGIN_PAGE) {
|
||||
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
} else {
|
||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
}
|
||||
|
||||
if (!currentProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
||||
<>
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2 mb-5' : 'alert-warning mt-n2 mb-5'}>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{message}</p>
|
||||
</Alert>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.defaultProps = {
|
||||
currentProvider: '',
|
||||
referrer: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
currentProvider: PropTypes.string,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ThirdPartyAuthAlert);
|
||||
export default ThirdPartyAuthAlert;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser, getSiteConfig } from '@openedx/frontend-base';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
DEFAULT_REDIRECT_URL,
|
||||
} from '../data/constants';
|
||||
|
||||
/**
|
||||
* This wrapper redirects the requester to our default redirect url if they are
|
||||
* already authenticated.
|
||||
*/
|
||||
const UnAuthOnlyRoute = (props) => {
|
||||
const UnAuthOnlyRoute = ({ children }) => {
|
||||
const [authUser, setAuthUser] = useState({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
@@ -21,14 +24,18 @@ const UnAuthOnlyRoute = (props) => {
|
||||
|
||||
if (isReady) {
|
||||
if (authUser && authUser.username) {
|
||||
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
global.location.href = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Route {...props} />;
|
||||
return children;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
UnAuthOnlyRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default UnAuthOnlyRoute;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
|
||||
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
|
||||
|
||||
// Third party auth context
|
||||
export const getThirdPartyAuthContext = (urlParams) => ({
|
||||
@@ -12,11 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, thirdPartyAuthContext) => ({
|
||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, thirdPartyAuthContext },
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextFailure = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
||||
});
|
||||
|
||||
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
});
|
||||
|
||||
@@ -1,32 +1,59 @@
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
||||
|
||||
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
||||
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
export const defaultState = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {
|
||||
fields: {},
|
||||
extended_profile: [],
|
||||
},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
autoSubmitRegForm: false,
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
welcomePageRedirectUrl: null,
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
};
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
|
||||
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
||||
fieldDescriptions: action.payload.fieldDescriptions?.fields,
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
};
|
||||
}
|
||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
thirdPartyAuthApiStatus: FAILURE_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...state.thirdPartyAuthContext,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...state.thirdPartyAuthContext,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { logError } from '@openedx/frontend-base';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
// Actions
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||
import {
|
||||
THIRD_PARTY_AUTH_CONTEXT,
|
||||
getThirdPartyAuthContextBegin,
|
||||
getThirdPartyAuthContextSuccess,
|
||||
getThirdPartyAuthContextFailure,
|
||||
getThirdPartyAuthContextSuccess,
|
||||
THIRD_PARTY_AUTH_CONTEXT,
|
||||
} from './actions';
|
||||
|
||||
// Services
|
||||
import {
|
||||
getThirdPartyAuthContext,
|
||||
} from './service';
|
||||
@@ -18,11 +15,12 @@ import {
|
||||
export function* fetchThirdPartyAuthContext(action) {
|
||||
try {
|
||||
yield put(getThirdPartyAuthContextBegin());
|
||||
const { fieldDescriptions, thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
const {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
fieldDescriptions, thirdPartyAuthContext,
|
||||
));
|
||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||
} catch (e) {
|
||||
yield put(getThirdPartyAuthContextFailure());
|
||||
logError(e);
|
||||
|
||||
@@ -14,7 +14,15 @@ export const fieldDescriptionSelector = createSelector(
|
||||
commonComponents => commonComponents.fieldDescriptions,
|
||||
);
|
||||
|
||||
export const extendedProfileSelector = createSelector(
|
||||
export const optionalFieldsSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.extendedProfile,
|
||||
commonComponents => commonComponents.optionalFields,
|
||||
);
|
||||
|
||||
export const tpaProvidersSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => ({
|
||||
providers: commonComponents.thirdPartyAuthContext.providers,
|
||||
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function getThirdPartyAuthContext(urlParams) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -11,18 +9,15 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(
|
||||
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
|
||||
`${getSiteConfig().lmsBaseUrl}/api/mfe_context`,
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
return {
|
||||
fieldDescriptions: data.registration_fields || {},
|
||||
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
|
||||
// and deployed update it to use data.context_data
|
||||
thirdPartyAuthContext: camelCaseObject(
|
||||
convertKeyNames(data.context_data || data, { fullname: 'name' }),
|
||||
),
|
||||
fieldDescriptions: data.registrationFields || {},
|
||||
optionalFields: data.optionalFields || {},
|
||||
thirdPartyAuthContext: data.contextData || {},
|
||||
};
|
||||
}
|
||||
|
||||
82
src/common-components/data/tests/reducer.test.js
Normal file
82
src/common-components/data/tests/reducer.test.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { PENDING_STATE } from '../../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
|
||||
import reducer from '../reducers';
|
||||
|
||||
describe('common components reducer', () => {
|
||||
it('test mfe context response', () => {
|
||||
const state = {
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
const fieldDescriptions = {
|
||||
fields: [],
|
||||
};
|
||||
const optionalFields = {
|
||||
fields: [],
|
||||
extended_profile: {},
|
||||
};
|
||||
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
|
||||
const action = {
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
...state,
|
||||
fieldDescriptions: [],
|
||||
optionalFields: {
|
||||
fields: [],
|
||||
extended_profile: {},
|
||||
},
|
||||
thirdPartyAuthApiStatus: 'complete',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear tpa context error message', () => {
|
||||
const state = {
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: 'An error occurred',
|
||||
},
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...state.thirdPartyAuthContext,
|
||||
errorMessage: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import { runSaga } from 'redux-saga';
|
||||
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
|
||||
import { initializeMockServices } from '../../../setupTest';
|
||||
import * as actions from '../actions';
|
||||
import { fetchThirdPartyAuthContext } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
const { loggingService } = initializeMockServices();
|
||||
|
||||
describe('fetchThirdPartyAuthContext', () => {
|
||||
const params = {
|
||||
@@ -26,7 +27,11 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
|
||||
it('should call service and dispatch success action', async () => {
|
||||
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
|
||||
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data, fieldDescriptions: {} }));
|
||||
.mockImplementation(() => Promise.resolve({
|
||||
thirdPartyAuthContext: data,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
}));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
@@ -38,7 +43,8 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([
|
||||
actions.getThirdPartyAuthContextBegin(),
|
||||
actions.getThirdPartyAuthContextSuccess({}, data),
|
||||
setCountryFromThirdPartyAuthContext(),
|
||||
actions.getThirdPartyAuthContextSuccess({}, {}, data),
|
||||
]);
|
||||
getThirdPartyAuthContext.mockClear();
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user