Compare commits
469 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7045ccd7a1 | |||
|
|
4e21db20b0 | ||
|
|
af6fe9fcdb | ||
|
|
1118dca6c8 | ||
|
|
59ac43b1cc | ||
|
|
aaa849a697 | ||
|
|
840b042606 | ||
|
|
93d62e35e8 | ||
|
|
0c305651b0 | ||
|
|
dfad1edcb9 | ||
|
|
af46ef756e | ||
|
|
c86165180f | ||
|
|
97063850c6 | ||
|
|
af252d2d3f | ||
|
|
9e850c3229 | ||
|
|
413454b6e6 | ||
|
|
ed7cc8a36e | ||
|
|
a8a4ad59ed | ||
|
|
9f2b2a4026 | ||
|
|
21953ec96d | ||
|
|
57671449b1 | ||
|
|
66012905b2 | ||
|
|
209d52ed60 | ||
|
|
6d424edbd5 | ||
|
|
5a4279a7f3 | ||
|
|
ef0ea1378d | ||
|
|
b8b39bffbc | ||
|
|
7f778adda9 | ||
|
|
4cf48d8ba8 | ||
|
|
2e82ba910d | ||
|
|
02533b0474 | ||
|
|
ecf7f1dfc1 | ||
|
|
dbec796ceb | ||
|
|
4a797a59cc | ||
|
|
a8a7348605 | ||
|
|
1ff9ecaf15 | ||
|
|
8f67fdba68 | ||
|
|
43684dce91 | ||
|
|
3ddfbab1ef | ||
|
|
030758e078 | ||
|
|
c56b945f0d | ||
|
|
69750674c3 | ||
|
|
813cbb3156 | ||
|
|
20aaa4f2e2 | ||
|
|
4dfb1b3053 | ||
|
|
171a770235 | ||
|
|
f47c1ed1e6 | ||
|
|
c63da3051b | ||
|
|
04b35786d4 | ||
|
|
657e9c0190 | ||
|
|
2874c9603f | ||
|
|
ec5381ea17 | ||
|
|
d5ac171a5b | ||
|
|
3be690b34b | ||
|
|
441e1542ad | ||
|
|
c1db3d409e | ||
|
|
0c343cfdf0 | ||
|
|
f740d0107e | ||
|
|
98bc20a282 | ||
|
|
ca15863c82 | ||
|
|
ff9cb1b238 | ||
|
|
67967156f4 | ||
|
|
e4720ff6b0 | ||
|
|
df704ce6d7 | ||
|
|
a2dc80ffb8 | ||
|
|
95efe7fedd | ||
|
|
8df7d928dd | ||
|
|
5e77a47708 | ||
|
|
8ef3a27a62 | ||
|
|
e0841996d0 | ||
|
|
90f2e2540e | ||
|
|
6a02c517b9 | ||
|
|
5076d55314 | ||
|
|
2f3b9b87ca | ||
|
|
7970561181 | ||
|
|
8d46de8fe3 | ||
|
|
8341f17d46 | ||
|
|
d7c3e5a687 | ||
|
|
07b1c5bde1 | ||
|
|
5512faa9b0 | ||
|
|
48c49fe0b2 | ||
|
|
8c7778218b | ||
|
|
0dedbbd589 | ||
|
|
ef0b101fea | ||
|
|
edb22316b8 | ||
|
|
227a97afa1 | ||
|
|
d01486e5f7 | ||
|
|
a58f1eaf19 | ||
|
|
a5024c3fde | ||
|
|
d7be18e717 | ||
|
|
5e405da37e | ||
|
|
901f39f42c | ||
|
|
346a636b76 | ||
|
|
34dcc88880 | ||
|
|
a229c34535 | ||
|
|
5d7b4fecf4 | ||
|
|
f04130a7c6 | ||
|
|
cb7774b325 | ||
|
|
3e4eb21d8c | ||
|
|
a346dccd4c | ||
|
|
c64a201072 | ||
|
|
6496642643 | ||
|
|
a6c36654b4 | ||
|
|
ae5253c822 | ||
|
|
e44001e945 | ||
|
|
e07cf665a4 | ||
|
|
8213ee7460 | ||
|
|
8a7d6eecdf | ||
|
|
a2497eeb22 | ||
|
|
a703abad76 | ||
|
|
3f4d987d12 | ||
|
|
45e551ea44 | ||
|
|
a0d7fd7cf2 | ||
|
|
97d1bdedfb | ||
|
|
4351a09c9f | ||
|
|
9647a74507 | ||
|
|
9cef77349f | ||
|
|
ad7c42bcf9 | ||
|
|
266386fe24 | ||
|
|
f42ee37e16 | ||
|
|
354f9fdc38 | ||
|
|
85b07acfb5 | ||
|
|
3e647f7394 | ||
|
|
848b0f37b9 | ||
|
|
818b3800aa | ||
|
|
63e47bc45a | ||
|
|
7ba5371f69 | ||
|
|
9f0c286897 | ||
|
|
4cc5b91d6d | ||
|
|
3d75a72f0c | ||
|
|
0541dc194e | ||
|
|
b92127fd12 | ||
|
|
b2b9f3fa00 | ||
|
|
b9b6282b4b | ||
|
|
8606585978 | ||
|
|
159072779f | ||
|
|
de843d330d | ||
|
|
d554de89ca | ||
|
|
72be96c230 | ||
|
|
564f34a7c6 | ||
|
|
ab15b3d2bf | ||
|
|
59db41c61e | ||
|
|
0415c00353 | ||
|
|
9d01c074e0 | ||
|
|
83c5b0258f | ||
|
|
45246ad5ee | ||
|
|
487b2590bd | ||
|
|
6cab3f3f3e | ||
|
|
e3c8ec027e | ||
|
|
1e899c1c48 | ||
|
|
370b193df3 | ||
|
|
58c34abd66 | ||
|
|
c9942c1552 | ||
|
|
432dbb5e6b | ||
|
|
02748fab13 | ||
|
|
3a5506c646 | ||
|
|
10619ceb5e | ||
|
|
2694492a7c | ||
|
|
be7d0d97e4 | ||
|
|
e6aa4be4f6 | ||
|
|
f2c236c828 | ||
|
|
db912e6dae | ||
|
|
abb08be08e | ||
|
|
6c6ccc7f20 | ||
|
|
2494ad2b57 | ||
|
|
3b2a2bfa95 | ||
|
|
30b91791e3 | ||
|
|
08592aeec7 | ||
|
|
c1d143ace2 | ||
|
|
54a879aec2 | ||
|
|
b5b37f1d64 | ||
|
|
36be99ace0 | ||
|
|
222bc19bd0 | ||
|
|
3d827e64ea | ||
|
|
8a247abd6a | ||
|
|
a6943fbaeb | ||
|
|
19292cd5b6 | ||
|
|
b6374a5c05 | ||
|
|
f6a4036b49 | ||
|
|
12a845ad33 | ||
|
|
a4a7456726 | ||
|
|
6fdf73fed3 | ||
|
|
32b8079744 | ||
|
|
b9f7fe74c8 | ||
|
|
5a259a76df | ||
|
|
d2ad5ee2a4 | ||
|
|
67fefe814b | ||
|
|
9a5b1fa5e7 | ||
|
|
55f21aeeaa | ||
|
|
fc24e61a1c | ||
|
|
bace8286fd | ||
|
|
665653e9a5 | ||
|
|
0cc2282c44 | ||
|
|
455ffd345c | ||
|
|
dde02a0739 | ||
|
|
81cb72f10b | ||
|
|
e285a91408 | ||
|
|
50142adb85 | ||
|
|
7281804fbd | ||
|
|
b446534992 | ||
|
|
3adc305aec | ||
|
|
65446ce9c3 | ||
|
|
f3637b5624 | ||
|
|
3c1d2152aa | ||
|
|
48b22ea41e | ||
|
|
ed76e40862 | ||
|
|
d5b07cc38f | ||
|
|
b9509bb890 | ||
|
|
c87a1049c8 | ||
|
|
e0c22e781d | ||
|
|
f1605d1f27 | ||
|
|
5d7826c26c | ||
|
|
5d075b0cdc | ||
|
|
1a86685f11 | ||
|
|
e31597509c | ||
|
|
c79c137fd6 | ||
|
|
76f735ed39 | ||
|
|
2298791aeb | ||
|
|
9f48ccc66b | ||
|
|
398479d2e7 | ||
|
|
3d6d815373 | ||
|
|
287bf50a46 | ||
|
|
677e872320 | ||
|
|
d8ecfd6fa3 | ||
|
|
9a6074868b | ||
|
|
077ecf38a4 | ||
|
|
7d6aa276ec | ||
|
|
115e69fad3 | ||
|
|
3e04f76c81 | ||
|
|
83eeb88eab | ||
|
|
a05570ed37 | ||
|
|
058a846978 | ||
|
|
04cd95824e | ||
|
|
678b95f60d | ||
|
|
599f513624 | ||
|
|
e8ce471f6f | ||
|
|
4e727748af | ||
|
|
79c5bbff68 | ||
|
|
a95b600a00 | ||
|
|
464e952a1e | ||
|
|
018ca18a4e | ||
|
|
4b89e7561f | ||
|
|
da08e721c2 | ||
|
|
d037e96a0c | ||
|
|
050dd30d8f | ||
|
|
3dd41030d3 | ||
|
|
27228f093d | ||
|
|
83f3241e26 | ||
|
|
e153aefc13 | ||
|
|
936c8714b7 | ||
|
|
6b18f28933 | ||
|
|
0ce451cfd2 | ||
|
|
42e831a693 | ||
|
|
9af7eaf587 | ||
|
|
3fbbde1044 | ||
|
|
c3f0282be4 | ||
|
|
ceb9c13542 | ||
|
|
7229bff3ff | ||
|
|
0695d4f400 | ||
|
|
de8783c708 | ||
|
|
e2540bc3a0 | ||
|
|
ffb5a765e2 | ||
|
|
952e543217 | ||
|
|
0e6a272f2b | ||
|
|
45a1da9f5e | ||
|
|
022515d1d2 | ||
|
|
2d737aae7f | ||
|
|
4c4db14eac | ||
|
|
911cea6a0e | ||
|
|
a52ddfd9bd | ||
|
|
8175ba897a | ||
|
|
cfda72b2e2 | ||
|
|
4483a734bc | ||
|
|
db1903cdce | ||
|
|
71851b13a6 | ||
|
|
6efa31092d | ||
|
|
c3541a3d79 | ||
|
|
dad01fcd78 | ||
|
|
30e6eed60d | ||
|
|
de69ed3dd9 | ||
|
|
1d55df323f | ||
|
|
4e718f85de | ||
|
|
a211547a1d | ||
|
|
784e9afccf | ||
|
|
4b23d8c4e4 | ||
|
|
6d02e63d08 | ||
|
|
b1feed2443 | ||
|
|
cabf4e3f27 | ||
|
|
78a40d47c1 | ||
|
|
c7178afe6b | ||
|
|
18a6840037 | ||
|
|
583a487c38 | ||
|
|
3276496523 | ||
|
|
7ab55175b5 | ||
|
|
72e82005c0 | ||
|
|
c4df727178 | ||
|
|
1f6766175d | ||
|
|
2ac8988a9b | ||
|
|
642be093c7 | ||
|
|
86939a2559 | ||
|
|
8ed18f3d69 | ||
|
|
061746da9f | ||
|
|
de77aa5f0c | ||
|
|
7034d10536 | ||
|
|
4ce7311809 | ||
|
|
e76f5b6937 | ||
|
|
f8fc794458 | ||
|
|
a5069edd94 | ||
|
|
2543926c95 | ||
|
|
c5eb43a2a5 | ||
|
|
256fa5c9d8 | ||
|
|
267cce9f89 | ||
|
|
4f59c80a12 | ||
|
|
59afd596ab | ||
|
|
0c83268163 | ||
|
|
c5f5fa9281 | ||
|
|
e247aee372 | ||
|
|
f6ae5a4bdd | ||
|
|
057d16d3c1 | ||
|
|
93bb38d0bd | ||
|
|
01405eaff9 | ||
|
|
59fa6e2a35 | ||
|
|
9fc3a0e835 | ||
|
|
22e157adf6 | ||
|
|
6b81f69eba | ||
|
|
cefa84006c | ||
|
|
a54309dd63 | ||
|
|
aeb0fd2be7 | ||
|
|
26eb2bb4c7 | ||
|
|
8083079954 | ||
|
|
d4fc8489ea | ||
|
|
4020a81bd4 | ||
|
|
acf1adba80 | ||
|
|
a204ff8c03 | ||
|
|
e8ccc4b707 | ||
|
|
4d86780c73 | ||
|
|
fdbb83f51e | ||
|
|
f6e4664d37 | ||
|
|
65177fcdcc | ||
|
|
7601249fb6 | ||
|
|
c8ef3dad93 | ||
|
|
b02fe00c71 | ||
|
|
4404aede33 | ||
|
|
1b0edb10c4 | ||
|
|
546adff45e | ||
|
|
94b14fd618 | ||
|
|
5b8a9a587b | ||
|
|
2650cb59b3 | ||
|
|
bc2b13175a | ||
|
|
85e8094833 | ||
|
|
aff8dda3ee | ||
|
|
51b505552d | ||
|
|
3648f1b6be | ||
|
|
c78b6964b9 | ||
|
|
664d05134b | ||
|
|
b969522cd0 | ||
|
|
0cd8210ea7 | ||
|
|
1c763c2102 | ||
|
|
073003284a | ||
|
|
92fdf85c9a | ||
|
|
5ee8a8c75c | ||
|
|
536d67404f | ||
|
|
9d99bfcec6 | ||
|
|
3180c9d973 | ||
|
|
1645274d9f | ||
|
|
84e43cb038 | ||
|
|
994b21c0c1 | ||
|
|
940b45ba7e | ||
|
|
4efa0a07ae | ||
|
|
2bd6879bda | ||
|
|
b479f0b376 | ||
|
|
dfdcbc0a8d | ||
|
|
c3b02a2946 | ||
|
|
f6c1a8bcc1 | ||
|
|
6c02962e0d | ||
|
|
acaf98f0b1 | ||
|
|
90351083aa | ||
|
|
6f75684ad9 | ||
|
|
a54f099d68 | ||
|
|
02d081dd26 | ||
|
|
468acc80f0 | ||
|
|
90fdd13fbc | ||
|
|
faf1b8522a | ||
|
|
e8a28b09bc | ||
|
|
c611df3f69 | ||
|
|
ab371f1c3a | ||
|
|
fb2002a004 | ||
|
|
f955ec4434 | ||
|
|
d529e00d7b | ||
|
|
e0cbbf7da1 | ||
|
|
ba209fd050 | ||
|
|
ae7004e95d | ||
|
|
de9eb63b07 | ||
|
|
bf64a829cc | ||
|
|
1e1b06dfa5 | ||
|
|
469a93bd9c | ||
|
|
0645761f05 | ||
|
|
20e9881546 | ||
|
|
42b347058f | ||
|
|
b097535580 | ||
|
|
d06929ca2b | ||
|
|
0a9316c407 | ||
|
|
d7704799fe | ||
|
|
cc26f750e8 | ||
|
|
8ac22515a6 | ||
|
|
b4a8d0afdc | ||
|
|
c131842139 | ||
|
|
acf5398b31 | ||
|
|
871e35d53a | ||
|
|
c8fb2ded56 | ||
|
|
01ffe0b945 | ||
|
|
092c61dada | ||
|
|
cc43299932 | ||
|
|
9086b96313 | ||
|
|
86b9d4a061 | ||
|
|
36fd9cb89a | ||
|
|
5aec913cd6 | ||
|
|
5da9cc3ce4 | ||
|
|
97e8c69345 | ||
|
|
a410ec8ed6 | ||
|
|
7e197e2315 | ||
|
|
6373bc726f | ||
|
|
1ccf3214f0 | ||
|
|
137701d10c | ||
|
|
37d39dba95 | ||
|
|
5d297a0d00 | ||
|
|
1aa6e80274 | ||
|
|
15778a5e4a | ||
|
|
48c41a7757 | ||
|
|
28ee7518f4 | ||
|
|
c35df7ef60 | ||
|
|
a8d5a0b62a | ||
|
|
64f55150b6 | ||
|
|
9ac38762eb | ||
|
|
72077302c0 | ||
|
|
f1a54ededf | ||
|
|
709844a459 | ||
|
|
032c68edf2 | ||
|
|
be8b6fba01 | ||
|
|
541fe55d6e | ||
|
|
28a62cf263 | ||
|
|
6e7584c89c | ||
|
|
49b03689a0 | ||
|
|
fdb00ce1f5 | ||
|
|
5d1d73a6b6 | ||
|
|
b292ee39ad | ||
|
|
c5f13022de | ||
|
|
47a3962118 | ||
|
|
b55d0d6bea | ||
|
|
8cb19859aa | ||
|
|
bafa6452ca | ||
|
|
3ab658ffa0 | ||
|
|
72b6a2c2a0 | ||
|
|
a479556795 | ||
|
|
8f03fa84ac | ||
|
|
990d82be6a | ||
|
|
b90db977a0 | ||
|
|
093452860e | ||
|
|
8ac9a2dc3c | ||
|
|
440eacbce4 | ||
|
|
725fa982c7 | ||
|
|
fe41fbc0b1 | ||
|
|
0de0a3f07f | ||
|
|
b0009fef42 | ||
|
|
8bfae8bf2f | ||
|
|
abe0bbbf51 | ||
|
|
c6e971ef1d | ||
|
|
2f0a07b7eb | ||
|
|
56593df3bd |
@@ -1,10 +1,13 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload
|
||||
ACCOUNT_PROFILE_URL=http://localhost:1995
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
BASE_URL=localhost:8080
|
||||
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
|
||||
STUDIO_BASE_URL=http://localhost:18010
|
||||
LOGIN_URL=http://localhost:18000/login
|
||||
LOGOUT_URL=http://localhost:18000/logout
|
||||
MARKETING_SITE_BASE_URL=http://localhost:18000
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
module.exports = createConfig('eslint');
|
||||
|
||||
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @edx/community-engineering
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
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 }}
|
||||
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/ci.yml
vendored
@@ -1,26 +1,39 @@
|
||||
name: Default CI
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
jobs:
|
||||
build:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 12
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Type check
|
||||
run: npm run types
|
||||
- name: Test
|
||||
run: npm run test
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: i18n_extract
|
||||
run: npm run i18n_extract
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
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
|
||||
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: lockfileVersion check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
72
.github/workflows/release.yml
vendored
@@ -2,38 +2,50 @@ name: Release CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- master
|
||||
- alpha
|
||||
permissions:
|
||||
contents: read # for checkout
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # to be able to publish a GitHub release
|
||||
issues: write # to be able to comment on released issues
|
||||
pull-requests: write # to be able to comment on released pull requests
|
||||
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Test
|
||||
run: npm run test
|
||||
- name: i18n_extract
|
||||
run: npm run i18n_extract
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
with:
|
||||
semantic_version: 16
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Type check
|
||||
run: npm run types
|
||||
- name: Test
|
||||
run: npm run test
|
||||
- name: i18n_extract
|
||||
run: npm run i18n_extract
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
run: npx semantic-release@25
|
||||
|
||||
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
|
||||
3
.gitignore
vendored
@@ -7,3 +7,6 @@ temp
|
||||
src/i18n/transifex_input.json
|
||||
module.config.js
|
||||
.idea/
|
||||
|
||||
.vscode
|
||||
src/i18n/messages
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"branches": [
|
||||
"master",
|
||||
{name: "alpha", prerelease: true}
|
||||
],
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-component-header]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
17
Complete Logo.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="518" height="87" viewBox="0 0 518 87" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512.654 43.4098C512.654 35.747 509.327 31.2325 503.564 31.2325C497.861 31.2325 494.474 35.747 494.474 43.4098C494.474 51.0726 497.861 55.5871 503.564 55.5871C509.327 55.5871 512.654 51.0726 512.654 43.4098ZM518 56.0029C518 67.111 513.188 72.3978 503.029 72.3978C499.286 72.3978 496.019 71.8632 492.87 70.6751V65.5072C496.019 67.1704 499.049 68.0021 502.198 68.0021C509.148 68.0021 512.654 64.3786 512.654 56.9534V54.3397C510.455 58.1414 507.01 60.0422 502.257 60.0422C498.277 60.0422 495.009 58.5572 492.573 55.5277C490.137 52.4982 488.949 48.4589 488.949 43.4098C488.949 38.3607 490.137 34.3807 492.573 31.3513C495.009 28.3218 498.277 26.7773 502.257 26.7773C507.01 26.7773 510.455 28.6782 512.654 32.4799V27.5496H518V56.0029Z" fill="black"/>
|
||||
<path d="M484.03 50.3004C484.149 55.7653 484.209 59.864 484.209 59.864L478.743 59.9234C478.743 59.9234 478.862 60.7551 478.683 51.0726C478.565 45.4294 478.565 40.6179 478.565 40.6179C478.565 34.4401 476.129 31.4107 471.376 31.4107C465.613 31.4107 462.049 35.3312 462.049 41.6871V60.0422H456.702C456.702 60.0422 456.702 46.7363 456.702 37.1726C456.702 32.5987 453.375 33.0145 453.375 33.0145V27.5496C453.375 27.5496 456.88 27.5496 458.841 29.688C460.564 31.5889 460.623 33.7273 460.623 33.7273C461.811 29.7474 466.802 26.7773 472.03 26.7773C479.456 26.7773 483.911 31.4701 483.911 40.4397C483.911 40.4397 483.911 41.5089 484.03 50.3004Z" fill="black"/>
|
||||
<path d="M443.611 27.5485H448.958C448.958 27.5485 448.661 42.8147 448.779 51.4873C448.779 55.5861 452.166 55.1702 452.166 55.1702V60.2194C452.166 60.2194 443.492 60.0412 443.611 52.2002C443.73 42.6365 443.611 27.5485 443.611 27.5485ZM446.225 14.3613C444.264 14.3613 442.66 15.9652 442.66 17.9254C442.66 19.8857 444.264 21.4895 446.225 21.4895C448.185 21.4895 449.789 19.9451 449.789 17.9254C449.789 15.9058 448.185 14.3613 446.225 14.3613Z" fill="black"/>
|
||||
<path d="M436.376 50.3004C436.495 55.7653 436.554 59.864 436.554 59.864L431.089 59.9234C431.089 59.9234 431.207 60.7551 431.029 51.0726C430.91 45.4294 430.91 40.6179 430.91 40.6179C430.91 34.4401 428.475 31.4107 423.722 31.4107C417.959 31.4107 414.395 35.3312 414.395 41.6871V60.0422H409.048C409.048 60.0422 409.048 46.7363 409.048 37.1726C409.048 32.5987 405.721 33.0145 405.721 33.0145V27.5496C405.721 27.5496 409.226 27.5496 411.186 29.688C412.909 31.5889 412.969 33.7273 412.969 33.7273C414.157 29.7474 419.147 26.7773 424.375 26.7773C431.801 26.7773 436.257 31.4701 436.257 40.4397C436.257 40.4397 436.257 41.5089 436.376 50.3004Z" fill="black"/>
|
||||
<path d="M403.931 32.4208C402.208 32.5396 400.961 32.5396 399 33.49C396.386 34.7969 393.237 37.7076 393.237 42.9349C393.237 49.8849 393.237 60.0426 393.237 60.0426H387.891V27.5499H393.237V32.599C395.257 29.5695 397.337 27.3717 401.911 27.1935C402.684 27.1341 402.446 27.1341 403.931 27.1935V32.4208Z" fill="black"/>
|
||||
<path d="M369.696 43.7068C365.359 43.7068 362.388 44.182 360.725 45.1918C359.061 46.2017 358.23 47.8649 358.23 50.241C358.23 54.0427 360.903 56.4187 365.181 56.4187C371.122 56.4187 374.983 51.8448 374.983 44.8948V43.7068H369.696ZM380.271 51.9636C380.271 56.1811 384.251 55.8247 384.251 55.8247V59.9234C384.251 59.9234 381.221 60.2799 379.142 58.6166C377.241 57.1316 376.112 54.9337 376.112 54.9337C373.676 58.9136 369.042 60.8739 363.755 60.8739C357.101 60.8739 352.883 56.8346 352.883 50.5974C352.883 43.291 357.814 39.5487 367.498 39.5487H374.983V39.0141C374.983 34.0837 372.191 31.2919 366.309 31.2919C363.577 31.2919 360.071 33.3709 360.19 36.935L355.259 37.0538C355.378 28.6782 362.923 26.7773 366.428 26.7773C375.577 26.7773 380.33 31.5889 380.33 41.5089C380.33 41.5089 380.211 47.8055 380.271 51.9636Z" fill="black"/>
|
||||
<path d="M349.423 42.4594V45.073H324.887C325.362 52.4388 329.402 56.3593 336.472 56.3593C340.571 56.3593 344.374 55.4089 348.176 53.3299V58.379C344.374 60.0422 340.274 60.8739 336.175 60.8739C331.006 60.8739 326.848 59.3888 323.818 56.3593C320.788 53.3299 319.303 49.2906 319.303 44.1226C319.303 38.8359 320.729 34.6184 323.58 31.4701C326.491 28.3218 330.353 26.7773 335.224 26.7773C339.621 26.7773 343.067 28.203 345.621 30.9949C348.176 33.7867 349.423 37.6478 349.423 42.4594ZM344.076 40.9149C344.017 35.0342 340.571 31.2919 335.284 31.2919C329.343 31.2919 325.6 34.7966 325.065 40.9149H344.076Z" fill="black"/>
|
||||
<path d="M308.35 14.897H313.696C313.696 14.897 313.875 38.4794 313.875 51.1914C313.875 55.4089 319.459 55.3495 319.459 55.3495L319.4 60.161C319.4 60.161 313.578 60.3392 310.488 56.0029C309.003 53.9832 308.35 51.726 308.35 48.7559C308.35 35.3905 308.35 14.897 308.35 14.897Z" fill="black"/>
|
||||
<path d="M279.439 14.897H284.786C284.786 14.897 284.965 38.4794 284.965 51.1914C284.965 55.4089 290.549 55.3495 290.549 55.3495L290.49 60.161C290.49 60.161 284.667 60.3392 281.578 56.0029C280.093 53.9832 279.439 51.726 279.439 48.7559C279.439 35.3905 279.439 14.897 279.439 14.897Z" fill="black"/>
|
||||
<path d="M261.065 43.7068C256.728 43.7068 253.757 44.182 252.094 45.1918C250.431 46.2017 249.599 47.8649 249.599 50.241C249.599 54.0427 252.272 56.4187 256.55 56.4187C262.491 56.4187 266.352 51.8448 266.352 44.8948V43.7068H261.065ZM271.64 51.9636C271.64 56.1811 275.62 55.8247 275.62 55.8247V59.9234C275.62 59.9234 272.59 60.2799 270.511 58.6166C268.61 57.1316 267.481 54.9337 267.481 54.9337C265.045 58.9136 260.411 60.8739 255.124 60.8739C248.47 60.8739 244.252 56.8346 244.252 50.5974C244.252 43.291 249.183 39.5487 258.867 39.5487H266.352V39.0141C266.352 34.0837 263.56 31.2919 257.678 31.2919C254.946 31.2919 251.44 33.3709 251.559 36.935L246.628 37.0538C246.747 28.6782 254.292 26.7773 257.797 26.7773C266.946 26.7773 271.699 31.5889 271.699 41.5089C271.699 41.5089 271.58 47.8055 271.64 51.9636Z" fill="black"/>
|
||||
<path d="M233.216 32.4799V14.897H238.563C238.563 14.897 238.682 38.42 238.682 50.8943C238.682 55.2901 238.682 59.864 238.682 59.864L233.81 59.9234L233.87 55.4089C231.671 59.27 227.572 60.8738 222.819 60.8738C218.898 60.8738 215.69 59.3294 213.195 56.1811C210.759 53.0328 209.512 48.9341 209.512 43.8256C209.512 38.717 210.759 34.6183 213.195 31.47C215.69 28.3217 218.898 26.7773 222.819 26.7773C227.572 26.7773 231.018 28.6187 233.216 32.4799ZM215.037 43.8256C215.037 51.6666 218.423 56.4187 224.126 56.4187C229.77 56.4187 233.216 51.6666 233.216 43.8256C233.216 35.9845 229.77 31.2324 224.126 31.2324C218.423 31.2324 215.037 35.9845 215.037 43.8256Z" fill="black"/>
|
||||
<path d="M204.591 50.3004C204.71 55.7653 204.769 59.864 204.769 59.864L199.303 59.9234C199.303 59.9234 199.422 60.7551 199.244 51.0726C199.125 45.4294 199.125 40.6179 199.125 40.6179C199.125 34.4401 196.689 31.4107 191.937 31.4107C186.174 31.4107 182.609 35.3312 182.609 41.6871V60.0422H177.262C177.262 60.0422 177.262 46.7363 177.262 37.1726C177.262 32.5987 173.936 33.0145 173.936 33.0145V27.5496C173.936 27.5496 177.441 27.5496 179.401 29.688C181.124 31.5889 181.183 33.7273 181.183 33.7273C182.372 29.7474 187.362 26.7773 192.59 26.7773C200.016 26.7773 204.472 31.4701 204.472 40.4397C204.472 40.4397 204.472 41.5089 204.591 50.3004Z" fill="black"/>
|
||||
<path d="M159.067 43.7068C154.73 43.7068 151.759 44.182 150.096 45.1918C148.432 46.2017 147.601 47.8649 147.601 50.241C147.601 54.0427 150.274 56.4187 154.552 56.4187C160.493 56.4187 164.354 51.8448 164.354 44.8948V43.7068H159.067ZM169.642 51.9636C169.642 56.1811 173.622 55.8247 173.622 55.8247V59.9234C173.622 59.9234 170.592 60.2799 168.513 58.6166C166.612 57.1316 165.483 54.9337 165.483 54.9337C163.047 58.9136 158.413 60.8739 153.126 60.8739C146.472 60.8739 142.254 56.8346 142.254 50.5974C142.254 43.291 147.185 39.5487 156.869 39.5487H164.354V39.0141C164.354 34.0837 161.562 31.2919 155.68 31.2919C152.948 31.2919 149.442 33.3709 149.561 36.935L144.63 37.0538C144.749 28.6782 152.294 26.7773 155.799 26.7773C164.948 26.7773 169.701 31.5889 169.701 41.5089C169.701 41.5089 169.582 47.8055 169.642 51.9636Z" fill="black"/>
|
||||
<path d="M8.40549 86.8917C3.76365 86.8917 0 83.0908 0 78.4008V55.9119C0 51.2229 3.76265 47.421 8.40549 47.421H121.955C121.91 52.4182 121.875 57.4144 121.851 62.4106C121.821 68.5717 121.808 74.7338 121.81 80.8959C124.658 80.8726 127.506 80.8482 130.355 80.8249V86.8907L8.40549 86.8917Z" fill="#FF4F00"/>
|
||||
<path d="M124.912 40.1348H9.02975C4.08481 40.1348 0.0752715 36.3339 0.0752715 31.6439V8.49088C0.0752715 3.80189 4.08481 0 9.02975 0H130.282C130.264 1.47615 130.246 2.95331 130.229 4.42945C129.018 4.43249 127.808 4.43554 126.596 4.43858C125.745 8.73218 124.895 13.0268 124.045 17.3204C122.613 23.2077 121.182 29.0951 119.749 34.9814H124.935C124.927 36.6989 124.919 38.4173 124.912 40.1348Z" fill="#FF4F00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
24
Makefile
@@ -1,14 +1,9 @@
|
||||
transifex_resource = frontend-component-header
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
build:
|
||||
rm -rf ./dist
|
||||
@@ -19,7 +14,7 @@ build:
|
||||
@rm -rf dist/__mocks__
|
||||
|
||||
requirements:
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -37,21 +32,6 @@ 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/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
|
||||
136
README.rst
@@ -2,22 +2,43 @@
|
||||
frontend-component-header
|
||||
#########################
|
||||
|
||||
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|license| |Build Status| |Codecov| |npm_version| |npm_downloads| |semantic-release|
|
||||
|
||||
********
|
||||
Overview
|
||||
Purpose
|
||||
********
|
||||
|
||||
A generic header for Open edX micro-frontend applications.
|
||||
|
||||
************
|
||||
Requirements
|
||||
Getting Started
|
||||
************
|
||||
|
||||
This component uses ``@edx/frontend-platform`` services such as i18n, analytics, configuration, and the ``AppContext`` React component, and expects that it has been loaded into a micro-frontend that has been properly initialized via ``@edx/frontend-platform``'s ``initialize`` function. `Please visit the frontend template application to see an example. <https://github.com/edx/frontend-template-application/blob/master/src/index.jsx>`_
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
The `devstack`_ is currently recommended as a development environment for your
|
||||
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
||||
everything you need as a companion to this frontend.
|
||||
|
||||
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
.. _Devstack: https://github.com/openedx/devstack
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
This component uses ``@edx/frontend-platform`` services such as i18n, analytics, configuration, and the ``AppContext`` React component, and expects that it has been loaded into a micro-frontend that has been properly initialized via ``@edx/frontend-platform``'s ``initialize`` function. `Please visit the frontend template application to see an example. <https://github.com/openedx/frontend-template-application/blob/master/src/index.jsx>`_
|
||||
|
||||
As of version 7.x, consuming applications must support typescript.
|
||||
|
||||
Environment Variables
|
||||
=====================
|
||||
====================
|
||||
|
||||
* ``LMS_BASE_URL`` - The URL of the LMS of your Open edX instance.
|
||||
* ``LOGOUT_URL`` - The URL of the API endpoint which performs a user logout.
|
||||
@@ -26,13 +47,14 @@ Environment Variables
|
||||
Defaults to "localhost" in development.
|
||||
* ``LOGO_URL`` - The URL of the site's logo. This logo is displayed in the header.
|
||||
* ``ORDER_HISTORY_URL`` - The URL of the order history page.
|
||||
* ``ACCOUNT_PROFILE_URL`` - The URL of the account profile page.
|
||||
* ``ACCOUNT_SETTINGS_URL`` - The URL of the account settings page.
|
||||
* ``AUTHN_MINIMAL_HEADER`` - A boolean flag which hides the main menu, user menu, and logged-out
|
||||
menu items when truthy. This is intended to be used in micro-frontends like
|
||||
frontend-app-authentication in which these menus are considered distractions from the user's task.
|
||||
|
||||
************
|
||||
Installation
|
||||
************
|
||||
============
|
||||
|
||||
To install this header into your Open edX micro-frontend, run the following command in your MFE:
|
||||
|
||||
@@ -40,9 +62,33 @@ To install this header into your Open edX micro-frontend, run the following comm
|
||||
|
||||
This will make the component available to be imported into your application.
|
||||
|
||||
*****
|
||||
Cloning and Startup
|
||||
===================
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
||||
1. Clone your new repo:
|
||||
|
||||
``git clone https://github.com/openedx/frontend-component-header.git``
|
||||
|
||||
2. Use node v24.x.
|
||||
|
||||
The current version of the micro-frontend build scripts support node 24.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an .nvmrc file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
3. Install npm dependencies:
|
||||
|
||||
``cd frontend-component-header && npm ci``
|
||||
|
||||
4. Start the dev server:
|
||||
|
||||
``npm start``
|
||||
|
||||
Usage
|
||||
*****
|
||||
=====
|
||||
|
||||
This library has the following exports:
|
||||
|
||||
@@ -50,19 +96,24 @@ This library has the following exports:
|
||||
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
|
||||
* ``dist/index.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.
|
||||
|
||||
Plugins
|
||||
-------
|
||||
This can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
The parts of this that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
* `An example of component and messages usage. <https://github.com/edx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L21>`_
|
||||
* `An example of SCSS file usage. <https://github.com/edx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L8>`_
|
||||
* `An example of component and messages usage. <https://github.com/openedx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L21>`_
|
||||
* `An example of SCSS file usage. <https://github.com/openedx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L8>`_
|
||||
|
||||
***********
|
||||
Development
|
||||
***********
|
||||
===========
|
||||
|
||||
Install dependencies::
|
||||
|
||||
npm i
|
||||
npm ci
|
||||
|
||||
Start the development server::
|
||||
|
||||
@@ -72,6 +123,63 @@ Build a production distribution::
|
||||
|
||||
npm run build
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The code in this repository is licensed under the AGPLv3 unless otherwise
|
||||
noted.
|
||||
|
||||
Please see `LICENSE <LICENSE>`_ for details.
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
||||
|
||||
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes,
|
||||
security fixes, maintenance work, or new features. However, please make sure
|
||||
to have a discussion about your new feature idea with the maintainers prior to
|
||||
beginning development to maximize the chances of your change being accepted.
|
||||
You can start a conversation by creating a new issue on this repo summarizing
|
||||
your idea.
|
||||
|
||||
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-component-header/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`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-component-header.svg?branch=master
|
||||
:target: https://travis-ci.com/edx/frontend-component-header
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-component-header
|
||||
|
||||
4
Standalone Logogram.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="81" height="54" viewBox="0 0 81 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.2088 53.8455C2.3323 53.8455 0 51.4901 0 48.5838V34.6478C0 31.7421 2.33168 29.3861 5.2088 29.3861H75.5746C75.5466 32.4828 75.5248 35.5788 75.5099 38.6749C75.4912 42.4928 75.4831 46.3114 75.4844 50.1299C77.2495 50.1155 79.0145 50.1004 80.7796 50.086V53.8448L5.2088 53.8455Z" fill="#FF4F00"/>
|
||||
<path d="M77.4068 24.8709H5.59565C2.53132 24.8709 0.046645 22.5156 0.046645 19.6092V5.26167C0.046645 2.35597 2.53132 0 5.59565 0H80.7342C80.7237 0.914745 80.7125 1.83012 80.7019 2.74486C79.9512 2.74675 79.2011 2.74864 78.4504 2.75052C77.923 5.4112 77.3962 8.0725 76.8695 10.7332C75.9819 14.3815 75.095 18.0298 74.2075 21.6775H77.4211C77.4161 22.7417 77.4112 23.8066 77.4068 24.8709Z" fill="#FF4F00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 806 B |
@@ -1,3 +1,3 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('babel-preserve-modules');
|
||||
|
||||
14
catalog-info.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# 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-component-header"
|
||||
description: "A generic header for the Open edX micro-frontend applications."
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
owner: group:committers-frontend
|
||||
type: "library"
|
||||
lifecycle: "production"
|
||||
BIN
docs/images/desktop_header.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
docs/images/mobile_main_menu.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/images/mobile_user_menu.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
111
docs/using_custom_header.rst
Normal file
@@ -0,0 +1,111 @@
|
||||
.. title:: Custom Header Component Documentation
|
||||
|
||||
Custom Header Component
|
||||
=======================
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
The ``Header`` component is used to display a header with a provided ``mainMenuItems``,
|
||||
``secondaryMenuItems``, and ``userMenuItems`` props. If props are provided, the component will use them; otherwise,
|
||||
if any of the props ``(mainMenuItems, secondaryMenuItems, userMenuItems)`` are not provided, default
|
||||
items will be displayed. This component provides flexibility in customization, making it suitable for a wide
|
||||
range of applications.
|
||||
|
||||
Props Details
|
||||
-------------
|
||||
|
||||
The `Header` component accepts the following **optional** props for customization:
|
||||
|
||||
``mainMenuItems``
|
||||
*****************
|
||||
|
||||
The main menu items is a list of menu items objects. On desktop screens, these items are displayed on the left side next to the logo icon.
|
||||
On mobile screens, the main menu is displayed as a dropdown menu triggered by a hamburger icon. The main menu dropdown appears below the logo when opened.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
[
|
||||
{ type: 'item', href: '/courses', content: 'Courses', isActive: true },
|
||||
{ type: 'item', href: '/programs', content: 'Programs' },
|
||||
{ type: 'item', href: '/discover', content: 'Discover New', disabled, true },
|
||||
{
|
||||
type: 'submenu',
|
||||
content: 'Sub Menu Item',
|
||||
submenuContent: (
|
||||
<>
|
||||
<div className="mb-1"><a rel="noopener" href="#">Submenu item 1</a></div>
|
||||
<div className="mb-1"><a rel="noopener" href="#">Submenu item 2</a></div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
**Submenu Implementation**
|
||||
|
||||
To implement a submenu, set the type to ``submenu`` and provide a ``submenuContent`` property.
|
||||
The submenuContent should be a React component (as shown in above example) that can be rendered.
|
||||
|
||||
**Note:**
|
||||
|
||||
- The ``type`` should be ``item`` or ``submenu``. If type is ``submenu``, it should contain ``submenuContent`` instead of ``href``.
|
||||
|
||||
- If any item is to be disabled, we can pass optional ``disabled: true`` in that item object and
|
||||
|
||||
- If any item is to be active, we can pass optional ``isActive: true`` in that item object
|
||||
|
||||
secondaryMenuItems
|
||||
******************
|
||||
|
||||
The secondary menu items has same structure as ``mainMenuItems``. On desktop screen, these items are displayed on the right of header just before the userMenu avatar and on mobile screen,
|
||||
these items are displayed below the mainMenu items in dropdown.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
[
|
||||
{ type: 'item', href: '/help', content: 'Help' },
|
||||
]
|
||||
|
||||
userMenuItems
|
||||
*************
|
||||
|
||||
The user menu items is list of objects. On desktop screens, these items are displayed as a dropdown menu on the most right side of the header. The dropdown is opened by clicking on the avatar icon, which is typically located at the far right of the header.
|
||||
On mobile screens, the user menu is also displayed as a dropdown menu, appearing under the avatar icon.
|
||||
|
||||
Each object represents a group in the user menu. Each object contains the ``heading`` and
|
||||
list of menu items to be displayed in that group. Heading is optional and will be displayed only if passed. There can
|
||||
be multiple groups. For a normal user menu, a single group can be passed with empty heading.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
[
|
||||
{
|
||||
heading: '',
|
||||
items: [
|
||||
{ type: 'item', href: '/profile', content: 'Profile' },
|
||||
{ type: 'item', href: '/logout', content: 'Logout' }
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
Screenshots
|
||||
***********
|
||||
|
||||
Desktop:
|
||||
|
||||
.. image:: ./images/desktop_header.png
|
||||
|
||||
Mobile:
|
||||
|
||||
.. image:: ./images/mobile_main_menu.png
|
||||
.. image:: ./images/mobile_user_menu.png
|
||||
|
||||
Some Important Notes
|
||||
--------------------
|
||||
|
||||
- Intl formatted strings should be passed in content attribute.
|
||||
- Only menu items in the main menu can be disabled.
|
||||
- Menu items in the main menu and user menu can have ``isActive`` prop.
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'babel-polyfill';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform';
|
||||
@@ -7,6 +5,7 @@ import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import './index.scss';
|
||||
import StudioHeader from '../src/studio-header/StudioHeader';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
@@ -32,7 +31,35 @@ subscribe(APP_READY, () => {
|
||||
}}>
|
||||
<Header />
|
||||
</AppContext.Provider>
|
||||
<h5 className="mt-2">Logged in state</h5>
|
||||
<h5 className="mt-2 mb-5">Logged in state</h5>
|
||||
<AppContext.Provider value={{
|
||||
authenticatedUser: {
|
||||
userId: '123abc',
|
||||
username: 'testuser',
|
||||
roles: [],
|
||||
administrator: false,
|
||||
},
|
||||
config: getConfig(),
|
||||
}}>
|
||||
<StudioHeader
|
||||
number="run123"
|
||||
org="testX"
|
||||
title="Course Name"
|
||||
isHiddenMainMenu={false}
|
||||
mainMenuDropdowns={[
|
||||
{
|
||||
id: 'content-dropdown',
|
||||
buttonTitle: 'Content',
|
||||
items: [{
|
||||
href: '#',
|
||||
title: 'Outline',
|
||||
}],
|
||||
},
|
||||
]}
|
||||
outlineLink="#"
|
||||
/>
|
||||
</AppContext.Provider>
|
||||
<h5 className="mt-2">Logged in state for Studio header</h5>
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
@import "@edx/brand/paragon/fonts";
|
||||
@import "@edx/brand/paragon/variables";
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
@import "@edx/brand/paragon/overrides";
|
||||
@use "@openedx/paragon/dist/core.min.css" as paragonCore;
|
||||
@use "@openedx/paragon/dist/light.min.css" as paragonLight;
|
||||
|
||||
@import "@edx/frontend-component-header/index";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# openedx.yaml
|
||||
|
||||
---
|
||||
owner: edx/fedx-team
|
||||
tags:
|
||||
- library
|
||||
- component
|
||||
- react
|
||||
45670
package-lock.json
generated
83
package.json
@@ -8,69 +8,66 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "make build",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage"
|
||||
"test": "fedx-scripts jest --coverage",
|
||||
"test:dev": "fedx-scripts jest --watchAll",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-component-header.git"
|
||||
"url": "git+https://github.com/openedx/frontend-component-header.git"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-component-header/issues"
|
||||
"url": "https://github.com/openedx/frontend-component-header/issues"
|
||||
},
|
||||
"homepage": "https://github.com/edx/frontend-component-header#readme",
|
||||
"homepage": "https://github.com/openedx/frontend-component-header#readme",
|
||||
"devDependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-build": "5.6.14",
|
||||
"@edx/frontend-platform": "1.14.1",
|
||||
"@edx/paragon": "12.8.0",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"husky": "7.0.4",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.6",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux": "4.1.2",
|
||||
"redux-saga": "1.1.3",
|
||||
"@testing-library/dom": "7.16.3",
|
||||
"@testing-library/jest-dom": "5.15.1",
|
||||
"jest": "27.3.1",
|
||||
"jest-chain": "1.1.5",
|
||||
"@testing-library/react": "10.3.0"
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@openedx/frontend-build": "^14.3.2",
|
||||
"@openedx/paragon": "^23.0.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"jest": "30.2.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "6.30.2",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux": "4.2.1",
|
||||
"redux-saga": "1.4.2",
|
||||
"ts-jest": "^29.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "6.26.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.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.14"
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-platform": "^1.8.0",
|
||||
"@edx/paragon": "^7.0.0",
|
||||
"@edx/frontend-platform": "^7.0.0 || ^8.0.0",
|
||||
"@openedx/paragon": ">= 22.0.0 < 24.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0"
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-router-dom": "^6.14.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
|
||||
@@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { AvatarIcon } from './Icons';
|
||||
|
||||
function Avatar({
|
||||
const Avatar = ({
|
||||
size,
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
}) {
|
||||
}) => {
|
||||
const avatar = src ? (
|
||||
<img className="d-block w-100 h-100" src={src} alt={alt} />
|
||||
) : (
|
||||
@@ -23,7 +23,7 @@ function Avatar({
|
||||
{avatar}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
|
||||
@@ -1,172 +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';
|
||||
|
||||
// Local Components
|
||||
import { Menu, MenuTrigger, MenuContent } from './Menu';
|
||||
import Avatar from './Avatar';
|
||||
import { LinkedLogo, Logo } from './Logo';
|
||||
|
||||
// i18n
|
||||
import messages from './Header.messages';
|
||||
|
||||
// Assets
|
||||
import { CaretIcon } from './Icons';
|
||||
|
||||
class DesktopHeader extends React.Component {
|
||||
constructor(props) { // eslint-disable-line no-useless-constructor
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
const { mainMenu } = this.props;
|
||||
|
||||
// Nodes are accepted as a prop
|
||||
if (!Array.isArray(mainMenu)) {
|
||||
return mainMenu;
|
||||
}
|
||||
|
||||
return mainMenu.map((menuItem) => {
|
||||
const {
|
||||
type,
|
||||
href,
|
||||
content,
|
||||
submenuContent,
|
||||
} = menuItem;
|
||||
|
||||
if (type === 'item') {
|
||||
return (
|
||||
<a key={`${type}-${content}`} className="nav-link" href={href}>{content}</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
|
||||
<MenuTrigger tag="a" className="nav-link d-inline-flex align-items-center" href={href}>
|
||||
{content} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||
</MenuTrigger>
|
||||
<MenuContent className="pin-left pin-right shadow py-2">
|
||||
{submenuContent}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderUserMenu() {
|
||||
const {
|
||||
userMenu,
|
||||
avatar,
|
||||
username,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
|
||||
<MenuTrigger
|
||||
tag="button"
|
||||
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
|
||||
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
|
||||
>
|
||||
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
|
||||
{username} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||
</MenuTrigger>
|
||||
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
|
||||
{userMenu.map(({ type, href, content }) => (
|
||||
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
|
||||
))}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoggedOutItems() {
|
||||
const { loggedOutItems } = this.props;
|
||||
|
||||
return loggedOutItems.map((item, i, arr) => (
|
||||
<a
|
||||
key={`${item.type}-${item.content}`}
|
||||
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
|
||||
href={item.href}
|
||||
>
|
||||
{item.content}
|
||||
</a>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
logo,
|
||||
logoAltText,
|
||||
logoDestination,
|
||||
loggedIn,
|
||||
intl,
|
||||
} = this.props;
|
||||
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
|
||||
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
|
||||
|
||||
return (
|
||||
<header className="site-header-desktop">
|
||||
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
|
||||
<div className={`container-fluid ${logoClasses}`}>
|
||||
<div className="nav-container position-relative d-flex align-items-center">
|
||||
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
|
||||
<nav
|
||||
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
|
||||
className="nav main-nav"
|
||||
>
|
||||
{this.renderMainMenu()}
|
||||
</nav>
|
||||
<nav
|
||||
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
|
||||
className="nav secondary-menu-container align-items-center ml-auto"
|
||||
>
|
||||
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DesktopHeader.propTypes = {
|
||||
mainMenu: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
})),
|
||||
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
})),
|
||||
logo: PropTypes.string,
|
||||
logoAltText: PropTypes.string,
|
||||
logoDestination: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
loggedIn: PropTypes.bool,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
DesktopHeader.defaultProps = {
|
||||
mainMenu: [],
|
||||
userMenu: [],
|
||||
loggedOutItems: [],
|
||||
logo: null,
|
||||
logoAltText: null,
|
||||
logoDestination: null,
|
||||
avatar: null,
|
||||
username: null,
|
||||
loggedIn: false,
|
||||
};
|
||||
|
||||
export default injectIntl(DesktopHeader);
|
||||
135
src/Header.jsx
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import Responsive from 'react-responsive';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
APP_CONFIG_INITIALIZED,
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
|
||||
import DesktopHeader from './DesktopHeader';
|
||||
import MobileHeader from './MobileHeader';
|
||||
import PropTypes from 'prop-types';
|
||||
import DesktopHeaderSlot from './plugin-slots/DesktopHeaderSlot';
|
||||
import MobileHeaderSlot from './plugin-slots/MobileHeaderSlot';
|
||||
|
||||
import messages from './Header.messages';
|
||||
|
||||
@@ -30,50 +31,69 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
|
||||
}, 'Header additional config');
|
||||
});
|
||||
|
||||
function Header({ intl }) {
|
||||
/**
|
||||
* Header component for the application.
|
||||
* Displays a header with the provided main menu, secondary menu, and user menu when the user is authenticated.
|
||||
* If any of the props (mainMenuItems, secondaryMenuItems, userMenuItems) are not provided, default
|
||||
* items are displayed.
|
||||
* For more details on how to use this component, please refer to this document:
|
||||
* https://github.com/openedx/frontend-component-header/blob/master/docs/using_custom_header.rst
|
||||
*
|
||||
* @param {list} mainMenuItems - The list of main menu items to display.
|
||||
* See the documentation for the structure of main menu item.
|
||||
* @param {list} secondaryMenuItems - The list of secondary menu items to display.
|
||||
* See the documentation for the structure of secondary menu item.
|
||||
* @param {list} userMenuItems - The list of user menu items to display.
|
||||
* See the documentation for the structure of user menu item.
|
||||
*/
|
||||
const Header = ({
|
||||
mainMenuItems, secondaryMenuItems, userMenuItems,
|
||||
}) => {
|
||||
const { authenticatedUser, config } = useContext(AppContext);
|
||||
const intl = useIntl();
|
||||
|
||||
const mainMenu = [
|
||||
const defaultMainMenu = [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['header.links.courses']),
|
||||
},
|
||||
];
|
||||
const defaultUserMenu = authenticatedUser === null ? [] : [{
|
||||
heading: '',
|
||||
items: [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['header.user.menu.dashboard']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
|
||||
content: intl.formatMessage(messages['header.user.menu.profile']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: config.ACCOUNT_SETTINGS_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.account.settings']),
|
||||
},
|
||||
// Users should only see Order History if have a ORDER_HISTORY_URL define in the environment.
|
||||
...(config.ORDER_HISTORY_URL ? [{
|
||||
type: 'item',
|
||||
href: config.ORDER_HISTORY_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.order.history']),
|
||||
}] : []),
|
||||
{
|
||||
type: 'item',
|
||||
href: config.LOGOUT_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.logout']),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
const orderHistoryItem = {
|
||||
type: 'item',
|
||||
href: config.ORDER_HISTORY_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.order.history']),
|
||||
};
|
||||
|
||||
const userMenu = authenticatedUser === null ? [] : [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['header.user.menu.dashboard']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/u/${authenticatedUser.username}`,
|
||||
content: intl.formatMessage(messages['header.user.menu.profile']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/account/settings`,
|
||||
content: intl.formatMessage(messages['header.user.menu.account.settings']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: config.LOGOUT_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.logout']),
|
||||
},
|
||||
];
|
||||
|
||||
// Users should only see Order History if have a ORDER_HISTORY_URL define in the environment.
|
||||
if (config.ORDER_HISTORY_URL) {
|
||||
userMenu.splice(-1, 0, orderHistoryItem);
|
||||
}
|
||||
const mainMenu = mainMenuItems || defaultMainMenu;
|
||||
const secondaryMenu = secondaryMenuItems || [];
|
||||
const userMenu = authenticatedUser === null ? [] : userMenuItems || defaultUserMenu;
|
||||
|
||||
const loggedOutItems = [
|
||||
{
|
||||
@@ -96,24 +116,47 @@ function Header({ intl }) {
|
||||
username: authenticatedUser !== null ? authenticatedUser.username : null,
|
||||
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
|
||||
mainMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : mainMenu,
|
||||
secondaryMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : secondaryMenu,
|
||||
userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu,
|
||||
loggedOutItems: getConfig().AUTHN_MINIMAL_HEADER ? [] : loggedOutItems,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Responsive maxWidth={768}>
|
||||
<MobileHeader {...props} />
|
||||
<Responsive maxWidth={769}>
|
||||
<MobileHeaderSlot props={props} />
|
||||
</Responsive>
|
||||
<Responsive minWidth={769}>
|
||||
<DesktopHeader {...props} />
|
||||
<DesktopHeaderSlot props={props} />
|
||||
</Responsive>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Header);
|
||||
Header.defaultProps = {
|
||||
mainMenuItems: null,
|
||||
secondaryMenuItems: null,
|
||||
userMenuItems: null,
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
mainMenuItems: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
secondaryMenuItems: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
userMenuItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
heading: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -56,6 +56,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Sign Up',
|
||||
description: 'Link to registration',
|
||||
},
|
||||
'header.user.menu.studio.home': {
|
||||
id: 'header.user.menu.studio.home',
|
||||
defaultMessage: 'Studio Home',
|
||||
description: 'Link to the Studio Home',
|
||||
},
|
||||
'header.label.account.nav': {
|
||||
id: 'header.label.account.nav',
|
||||
defaultMessage: 'Account',
|
||||
@@ -96,6 +101,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Skip to main content',
|
||||
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
|
||||
},
|
||||
'header.label.app.nav': {
|
||||
id: 'header.label.app.nav',
|
||||
defaultMessage: 'App',
|
||||
description: 'The aria label for the app Nav',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
@@ -6,119 +7,98 @@ import { Context as ResponsiveContext } from 'react-responsive';
|
||||
|
||||
import Header from './index';
|
||||
|
||||
const HeaderComponent = ({ width, contextValue }) => (
|
||||
<ResponsiveContext.Provider value={width}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<Header />
|
||||
</AppContext.Provider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
);
|
||||
|
||||
describe('<Header />', () => {
|
||||
it('renders correctly for anonymous desktop', () => {
|
||||
const component = (
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: null,
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
</AppContext.Provider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
);
|
||||
const contextValue = {
|
||||
authenticatedUser: null,
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
};
|
||||
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
|
||||
|
||||
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
|
||||
const wrapper = TestRenderer.create(component);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for authenticated desktop', () => {
|
||||
const component = (
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: {
|
||||
userId: 'abc123',
|
||||
username: 'edX',
|
||||
roles: [],
|
||||
administrator: false,
|
||||
},
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
</AppContext.Provider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
);
|
||||
const contextValue = {
|
||||
authenticatedUser: {
|
||||
userId: 'abc123',
|
||||
username: 'edX',
|
||||
roles: [],
|
||||
administrator: false,
|
||||
},
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
};
|
||||
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
|
||||
|
||||
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
|
||||
const wrapper = TestRenderer.create(component);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for anonymous mobile', () => {
|
||||
const component = (
|
||||
<ResponsiveContext.Provider value={{ width: 500 }}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: null,
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
</AppContext.Provider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
);
|
||||
const contextValue = {
|
||||
authenticatedUser: null,
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
};
|
||||
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
|
||||
|
||||
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
|
||||
const wrapper = TestRenderer.create(component);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for authenticated mobile', () => {
|
||||
const component = (
|
||||
<ResponsiveContext.Provider value={{ width: 500 }}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: {
|
||||
userId: 'abc123',
|
||||
username: 'edX',
|
||||
roles: [],
|
||||
administrator: false,
|
||||
},
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
</AppContext.Provider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
);
|
||||
const contextValue = {
|
||||
authenticatedUser: {
|
||||
userId: 'abc123',
|
||||
username: 'edX',
|
||||
roles: [],
|
||||
administrator: false,
|
||||
},
|
||||
config: {
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
},
|
||||
};
|
||||
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
|
||||
|
||||
const wrapper = TestRenderer.create(component);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
export const MenuIcon = props => (
|
||||
export const MenuIcon = (props) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
@@ -14,7 +14,7 @@ export const MenuIcon = props => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AvatarIcon = props => (
|
||||
export const AvatarIcon = (props) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
@@ -29,7 +29,7 @@ export const AvatarIcon = props => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CaretIcon = props => (
|
||||
export const CaretIcon = (props) => (
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
|
||||
30
src/Logo.jsx
@@ -1,35 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function Logo({ src, alt, ...attributes }) {
|
||||
return (
|
||||
<img src={src} alt={alt} {...attributes} />
|
||||
);
|
||||
}
|
||||
|
||||
Logo.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function LinkedLogo({
|
||||
const Logo = ({
|
||||
href,
|
||||
src,
|
||||
alt,
|
||||
...attributes
|
||||
}) {
|
||||
return (
|
||||
<a href={href} {...attributes}>
|
||||
<img className="d-block" src={src} alt={alt} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<a href={href} className="logo" {...attributes}>
|
||||
<img className="d-block" src={src} alt={alt} />
|
||||
</a>
|
||||
);
|
||||
|
||||
LinkedLogo.propTypes = {
|
||||
export const logoDataShape = {
|
||||
href: PropTypes.string.isRequired,
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { LinkedLogo, Logo };
|
||||
Logo.propTypes = logoDataShape;
|
||||
|
||||
export default Logo;
|
||||
|
||||
@@ -2,12 +2,10 @@ import React from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function MenuTrigger({ tag, className, ...attributes }) {
|
||||
return React.createElement(tag, {
|
||||
className: `menu-trigger ${className}`,
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
const MenuTrigger = ({ tag, className, ...attributes }) => React.createElement(tag, {
|
||||
className: `menu-trigger ${className}`,
|
||||
...attributes,
|
||||
});
|
||||
MenuTrigger.propTypes = {
|
||||
tag: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
@@ -16,14 +14,13 @@ MenuTrigger.defaultProps = {
|
||||
tag: 'div',
|
||||
className: null,
|
||||
};
|
||||
const MenuTriggerType = <MenuTrigger />.type;
|
||||
const MenuTriggerComp = <MenuTrigger />;
|
||||
const MenuTriggerType = MenuTriggerComp.type;
|
||||
|
||||
function MenuContent({ tag, className, ...attributes }) {
|
||||
return React.createElement(tag, {
|
||||
className: ['menu-content', className].join(' '),
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
const MenuContent = ({ tag, className, ...attributes }) => React.createElement(tag, {
|
||||
className: ['menu-content', className].join(' '),
|
||||
...attributes,
|
||||
});
|
||||
MenuContent.propTypes = {
|
||||
tag: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
.menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
background: var(--pgn-color-white, #fff);
|
||||
min-width: 10rem;
|
||||
|
||||
&.pin-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.pin-right {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.menu-dropdown-enter {
|
||||
opacity: 0;
|
||||
transform-origin: 75% 0;
|
||||
transform: scale3d(0.8, 0.8, 1);
|
||||
}
|
||||
|
||||
.menu-dropdown-enter-active {
|
||||
transform-origin: 75% 0;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
opacity: 1;
|
||||
}
|
||||
.menu-dropdown-enter-done {
|
||||
}
|
||||
|
||||
.menu-dropdown-exit {
|
||||
transform-origin: 75% 0;
|
||||
transform: scale3d(1, 1, 1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-dropdown-exit-active {
|
||||
transform-origin: 75% 0;
|
||||
transform: scale3d(0.8, 0.8, 1);
|
||||
transition: all 250ms cubic-bezier(0.8, 0, 0.6, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
.menu-dropdown-exit-done {
|
||||
}
|
||||
|
||||
@@ -1,197 +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';
|
||||
|
||||
// Local Components
|
||||
import { Menu, MenuTrigger, MenuContent } from './Menu';
|
||||
import Avatar from './Avatar';
|
||||
import { LinkedLogo, Logo } from './Logo';
|
||||
|
||||
// i18n
|
||||
import messages from './Header.messages';
|
||||
|
||||
// Assets
|
||||
import { MenuIcon } from './Icons';
|
||||
|
||||
class MobileHeader extends React.Component {
|
||||
constructor(props) { // eslint-disable-line no-useless-constructor
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
const { mainMenu } = this.props;
|
||||
|
||||
// Nodes are accepted as a prop
|
||||
if (!Array.isArray(mainMenu)) {
|
||||
return mainMenu;
|
||||
}
|
||||
|
||||
return mainMenu.map((menuItem) => {
|
||||
const {
|
||||
type,
|
||||
href,
|
||||
content,
|
||||
submenuContent,
|
||||
} = menuItem;
|
||||
|
||||
if (type === 'item') {
|
||||
return (
|
||||
<a key={`${type}-${content}`} className="nav-link" href={href}>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
|
||||
<MenuTrigger tag="a" role="button" tabIndex="0" className="nav-link">
|
||||
{content}
|
||||
</MenuTrigger>
|
||||
<MenuContent className="position-static pin-left pin-right py-2">
|
||||
{submenuContent}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderUserMenuItems() {
|
||||
const { userMenu } = this.props;
|
||||
|
||||
return userMenu.map(({ type, href, content }) => (
|
||||
<li className="nav-item" key={`${type}-${content}`}>
|
||||
<a className="nav-link" href={href}>{content}</a>
|
||||
</li>
|
||||
));
|
||||
}
|
||||
|
||||
renderLoggedOutItems() {
|
||||
const { loggedOutItems } = this.props;
|
||||
|
||||
return loggedOutItems.map(({ type, href, content }, i, arr) => (
|
||||
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
|
||||
<a
|
||||
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
|
||||
href={href}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
</li>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
logo,
|
||||
logoAltText,
|
||||
logoDestination,
|
||||
loggedIn,
|
||||
avatar,
|
||||
username,
|
||||
stickyOnMobile,
|
||||
intl,
|
||||
mainMenu,
|
||||
userMenu,
|
||||
loggedOutItems,
|
||||
} = this.props;
|
||||
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
|
||||
const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
|
||||
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'justify-content-left pl-3' : 'justify-content-center';
|
||||
|
||||
return (
|
||||
<header
|
||||
aria-label={intl.formatMessage(messages['header.label.main.header'])}
|
||||
className={`site-header-mobile d-flex justify-content-between align-items-center shadow ${stickyClassName}`}
|
||||
>
|
||||
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
|
||||
{mainMenu.length > 0 ? (
|
||||
<div className="w-100 d-flex justify-content-start">
|
||||
|
||||
<Menu className="position-static">
|
||||
<MenuTrigger
|
||||
tag="button"
|
||||
className="icon-button"
|
||||
aria-label={intl.formatMessage(messages['header.label.main.menu'])}
|
||||
title={intl.formatMessage(messages['header.label.main.menu'])}
|
||||
>
|
||||
<MenuIcon role="img" aria-hidden focusable="false" style={{ width: '1.5rem', height: '1.5rem' }} />
|
||||
</MenuTrigger>
|
||||
<MenuContent
|
||||
tag="nav"
|
||||
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
|
||||
className="nav flex-column pin-left pin-right border-top shadow py-2"
|
||||
>
|
||||
{this.renderMainMenu()}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`w-100 d-flex ${logoClasses}`}>
|
||||
{ logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} itemType="http://schema.org/Organization" />}
|
||||
</div>
|
||||
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
|
||||
<div className="w-100 d-flex justify-content-end align-items-center">
|
||||
<Menu tag="nav" aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} className="position-static">
|
||||
<MenuTrigger
|
||||
tag="button"
|
||||
className="icon-button"
|
||||
aria-label={intl.formatMessage(messages['header.label.account.menu'])}
|
||||
title={intl.formatMessage(messages['header.label.account.menu'])}
|
||||
>
|
||||
<Avatar size="1.5rem" src={avatar} alt={username} />
|
||||
</MenuTrigger>
|
||||
<MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2">
|
||||
{loggedIn ? this.renderUserMenuItems() : this.renderLoggedOutItems()}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MobileHeader.propTypes = {
|
||||
mainMenu: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
|
||||
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
})),
|
||||
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
})),
|
||||
logo: PropTypes.string,
|
||||
logoAltText: PropTypes.string,
|
||||
logoDestination: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
loggedIn: PropTypes.bool,
|
||||
stickyOnMobile: PropTypes.bool,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
MobileHeader.defaultProps = {
|
||||
mainMenu: [],
|
||||
userMenu: [],
|
||||
loggedOutItems: [],
|
||||
logo: null,
|
||||
logoAltText: null,
|
||||
logoDestination: null,
|
||||
avatar: null,
|
||||
username: null,
|
||||
loggedIn: false,
|
||||
stickyOnMobile: true,
|
||||
|
||||
};
|
||||
|
||||
export default injectIntl(MobileHeader);
|
||||
@@ -33,6 +33,7 @@ exports[`<Header /> renders correctly for anonymous desktop 1`] = `
|
||||
<a
|
||||
className="nav-link"
|
||||
href="http://localhost:18000/dashboard"
|
||||
onClick={null}
|
||||
>
|
||||
Courses
|
||||
</a>
|
||||
@@ -93,7 +94,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
|
||||
height="24px"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5rem",
|
||||
"width": "1.5rem",
|
||||
}
|
||||
@@ -163,7 +164,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
|
||||
<span
|
||||
className="avatar overflow-hidden d-inline-flex rounded-circle null"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5rem",
|
||||
"width": "1.5rem",
|
||||
}
|
||||
@@ -175,7 +176,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
|
||||
height="24px"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5rem",
|
||||
"width": "1.5rem",
|
||||
}
|
||||
@@ -229,6 +230,7 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
|
||||
<a
|
||||
className="nav-link"
|
||||
href="http://localhost:18000/dashboard"
|
||||
onClick={null}
|
||||
>
|
||||
Courses
|
||||
</a>
|
||||
@@ -253,7 +255,7 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
|
||||
<span
|
||||
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5em",
|
||||
"width": "1.5em",
|
||||
}
|
||||
@@ -265,7 +267,7 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
|
||||
height="24px"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5em",
|
||||
"width": "1.5em",
|
||||
}
|
||||
@@ -339,7 +341,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
|
||||
height="24px"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5rem",
|
||||
"width": "1.5rem",
|
||||
}
|
||||
@@ -409,7 +411,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
|
||||
<span
|
||||
className="avatar overflow-hidden d-inline-flex rounded-circle null"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5rem",
|
||||
"width": "1.5rem",
|
||||
}
|
||||
@@ -421,7 +423,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
|
||||
height="24px"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "1.5rem",
|
||||
"width": "1.5rem",
|
||||
}
|
||||
|
||||
17
src/assets/complete-logo.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="518" height="87" viewBox="0 0 518 87" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512.654 43.4098C512.654 35.747 509.327 31.2325 503.564 31.2325C497.861 31.2325 494.474 35.747 494.474 43.4098C494.474 51.0726 497.861 55.5871 503.564 55.5871C509.327 55.5871 512.654 51.0726 512.654 43.4098ZM518 56.0029C518 67.111 513.188 72.3978 503.029 72.3978C499.286 72.3978 496.019 71.8632 492.87 70.6751V65.5072C496.019 67.1704 499.049 68.0021 502.198 68.0021C509.148 68.0021 512.654 64.3786 512.654 56.9534V54.3397C510.455 58.1414 507.01 60.0422 502.257 60.0422C498.277 60.0422 495.009 58.5572 492.573 55.5277C490.137 52.4982 488.949 48.4589 488.949 43.4098C488.949 38.3607 490.137 34.3807 492.573 31.3513C495.009 28.3218 498.277 26.7773 502.257 26.7773C507.01 26.7773 510.455 28.6782 512.654 32.4799V27.5496H518V56.0029Z" fill="black"/>
|
||||
<path d="M484.03 50.3004C484.149 55.7653 484.209 59.864 484.209 59.864L478.743 59.9234C478.743 59.9234 478.862 60.7551 478.683 51.0726C478.565 45.4294 478.565 40.6179 478.565 40.6179C478.565 34.4401 476.129 31.4107 471.376 31.4107C465.613 31.4107 462.049 35.3312 462.049 41.6871V60.0422H456.702C456.702 60.0422 456.702 46.7363 456.702 37.1726C456.702 32.5987 453.375 33.0145 453.375 33.0145V27.5496C453.375 27.5496 456.88 27.5496 458.841 29.688C460.564 31.5889 460.623 33.7273 460.623 33.7273C461.811 29.7474 466.802 26.7773 472.03 26.7773C479.456 26.7773 483.911 31.4701 483.911 40.4397C483.911 40.4397 483.911 41.5089 484.03 50.3004Z" fill="black"/>
|
||||
<path d="M443.611 27.5485H448.958C448.958 27.5485 448.661 42.8147 448.779 51.4873C448.779 55.5861 452.166 55.1702 452.166 55.1702V60.2194C452.166 60.2194 443.492 60.0412 443.611 52.2002C443.73 42.6365 443.611 27.5485 443.611 27.5485ZM446.225 14.3613C444.264 14.3613 442.66 15.9652 442.66 17.9254C442.66 19.8857 444.264 21.4895 446.225 21.4895C448.185 21.4895 449.789 19.9451 449.789 17.9254C449.789 15.9058 448.185 14.3613 446.225 14.3613Z" fill="black"/>
|
||||
<path d="M436.376 50.3004C436.495 55.7653 436.554 59.864 436.554 59.864L431.089 59.9234C431.089 59.9234 431.207 60.7551 431.029 51.0726C430.91 45.4294 430.91 40.6179 430.91 40.6179C430.91 34.4401 428.475 31.4107 423.722 31.4107C417.959 31.4107 414.395 35.3312 414.395 41.6871V60.0422H409.048C409.048 60.0422 409.048 46.7363 409.048 37.1726C409.048 32.5987 405.721 33.0145 405.721 33.0145V27.5496C405.721 27.5496 409.226 27.5496 411.186 29.688C412.909 31.5889 412.969 33.7273 412.969 33.7273C414.157 29.7474 419.147 26.7773 424.375 26.7773C431.801 26.7773 436.257 31.4701 436.257 40.4397C436.257 40.4397 436.257 41.5089 436.376 50.3004Z" fill="black"/>
|
||||
<path d="M403.931 32.4208C402.208 32.5396 400.961 32.5396 399 33.49C396.386 34.7969 393.237 37.7076 393.237 42.9349C393.237 49.8849 393.237 60.0426 393.237 60.0426H387.891V27.5499H393.237V32.599C395.257 29.5695 397.337 27.3717 401.911 27.1935C402.684 27.1341 402.446 27.1341 403.931 27.1935V32.4208Z" fill="black"/>
|
||||
<path d="M369.696 43.7068C365.359 43.7068 362.388 44.182 360.725 45.1918C359.061 46.2017 358.23 47.8649 358.23 50.241C358.23 54.0427 360.903 56.4187 365.181 56.4187C371.122 56.4187 374.983 51.8448 374.983 44.8948V43.7068H369.696ZM380.271 51.9636C380.271 56.1811 384.251 55.8247 384.251 55.8247V59.9234C384.251 59.9234 381.221 60.2799 379.142 58.6166C377.241 57.1316 376.112 54.9337 376.112 54.9337C373.676 58.9136 369.042 60.8739 363.755 60.8739C357.101 60.8739 352.883 56.8346 352.883 50.5974C352.883 43.291 357.814 39.5487 367.498 39.5487H374.983V39.0141C374.983 34.0837 372.191 31.2919 366.309 31.2919C363.577 31.2919 360.071 33.3709 360.19 36.935L355.259 37.0538C355.378 28.6782 362.923 26.7773 366.428 26.7773C375.577 26.7773 380.33 31.5889 380.33 41.5089C380.33 41.5089 380.211 47.8055 380.271 51.9636Z" fill="black"/>
|
||||
<path d="M349.423 42.4594V45.073H324.887C325.362 52.4388 329.402 56.3593 336.472 56.3593C340.571 56.3593 344.374 55.4089 348.176 53.3299V58.379C344.374 60.0422 340.274 60.8739 336.175 60.8739C331.006 60.8739 326.848 59.3888 323.818 56.3593C320.788 53.3299 319.303 49.2906 319.303 44.1226C319.303 38.8359 320.729 34.6184 323.58 31.4701C326.491 28.3218 330.353 26.7773 335.224 26.7773C339.621 26.7773 343.067 28.203 345.621 30.9949C348.176 33.7867 349.423 37.6478 349.423 42.4594ZM344.076 40.9149C344.017 35.0342 340.571 31.2919 335.284 31.2919C329.343 31.2919 325.6 34.7966 325.065 40.9149H344.076Z" fill="black"/>
|
||||
<path d="M308.35 14.897H313.696C313.696 14.897 313.875 38.4794 313.875 51.1914C313.875 55.4089 319.459 55.3495 319.459 55.3495L319.4 60.161C319.4 60.161 313.578 60.3392 310.488 56.0029C309.003 53.9832 308.35 51.726 308.35 48.7559C308.35 35.3905 308.35 14.897 308.35 14.897Z" fill="black"/>
|
||||
<path d="M279.439 14.897H284.786C284.786 14.897 284.965 38.4794 284.965 51.1914C284.965 55.4089 290.549 55.3495 290.549 55.3495L290.49 60.161C290.49 60.161 284.667 60.3392 281.578 56.0029C280.093 53.9832 279.439 51.726 279.439 48.7559C279.439 35.3905 279.439 14.897 279.439 14.897Z" fill="black"/>
|
||||
<path d="M261.065 43.7068C256.728 43.7068 253.757 44.182 252.094 45.1918C250.431 46.2017 249.599 47.8649 249.599 50.241C249.599 54.0427 252.272 56.4187 256.55 56.4187C262.491 56.4187 266.352 51.8448 266.352 44.8948V43.7068H261.065ZM271.64 51.9636C271.64 56.1811 275.62 55.8247 275.62 55.8247V59.9234C275.62 59.9234 272.59 60.2799 270.511 58.6166C268.61 57.1316 267.481 54.9337 267.481 54.9337C265.045 58.9136 260.411 60.8739 255.124 60.8739C248.47 60.8739 244.252 56.8346 244.252 50.5974C244.252 43.291 249.183 39.5487 258.867 39.5487H266.352V39.0141C266.352 34.0837 263.56 31.2919 257.678 31.2919C254.946 31.2919 251.44 33.3709 251.559 36.935L246.628 37.0538C246.747 28.6782 254.292 26.7773 257.797 26.7773C266.946 26.7773 271.699 31.5889 271.699 41.5089C271.699 41.5089 271.58 47.8055 271.64 51.9636Z" fill="black"/>
|
||||
<path d="M233.216 32.4799V14.897H238.563C238.563 14.897 238.682 38.42 238.682 50.8943C238.682 55.2901 238.682 59.864 238.682 59.864L233.81 59.9234L233.87 55.4089C231.671 59.27 227.572 60.8738 222.819 60.8738C218.898 60.8738 215.69 59.3294 213.195 56.1811C210.759 53.0328 209.512 48.9341 209.512 43.8256C209.512 38.717 210.759 34.6183 213.195 31.47C215.69 28.3217 218.898 26.7773 222.819 26.7773C227.572 26.7773 231.018 28.6187 233.216 32.4799ZM215.037 43.8256C215.037 51.6666 218.423 56.4187 224.126 56.4187C229.77 56.4187 233.216 51.6666 233.216 43.8256C233.216 35.9845 229.77 31.2324 224.126 31.2324C218.423 31.2324 215.037 35.9845 215.037 43.8256Z" fill="black"/>
|
||||
<path d="M204.591 50.3004C204.71 55.7653 204.769 59.864 204.769 59.864L199.303 59.9234C199.303 59.9234 199.422 60.7551 199.244 51.0726C199.125 45.4294 199.125 40.6179 199.125 40.6179C199.125 34.4401 196.689 31.4107 191.937 31.4107C186.174 31.4107 182.609 35.3312 182.609 41.6871V60.0422H177.262C177.262 60.0422 177.262 46.7363 177.262 37.1726C177.262 32.5987 173.936 33.0145 173.936 33.0145V27.5496C173.936 27.5496 177.441 27.5496 179.401 29.688C181.124 31.5889 181.183 33.7273 181.183 33.7273C182.372 29.7474 187.362 26.7773 192.59 26.7773C200.016 26.7773 204.472 31.4701 204.472 40.4397C204.472 40.4397 204.472 41.5089 204.591 50.3004Z" fill="black"/>
|
||||
<path d="M159.067 43.7068C154.73 43.7068 151.759 44.182 150.096 45.1918C148.432 46.2017 147.601 47.8649 147.601 50.241C147.601 54.0427 150.274 56.4187 154.552 56.4187C160.493 56.4187 164.354 51.8448 164.354 44.8948V43.7068H159.067ZM169.642 51.9636C169.642 56.1811 173.622 55.8247 173.622 55.8247V59.9234C173.622 59.9234 170.592 60.2799 168.513 58.6166C166.612 57.1316 165.483 54.9337 165.483 54.9337C163.047 58.9136 158.413 60.8739 153.126 60.8739C146.472 60.8739 142.254 56.8346 142.254 50.5974C142.254 43.291 147.185 39.5487 156.869 39.5487H164.354V39.0141C164.354 34.0837 161.562 31.2919 155.68 31.2919C152.948 31.2919 149.442 33.3709 149.561 36.935L144.63 37.0538C144.749 28.6782 152.294 26.7773 155.799 26.7773C164.948 26.7773 169.701 31.5889 169.701 41.5089C169.701 41.5089 169.582 47.8055 169.642 51.9636Z" fill="black"/>
|
||||
<path d="M8.40549 86.8917C3.76365 86.8917 0 83.0908 0 78.4008V55.9119C0 51.2229 3.76265 47.421 8.40549 47.421H121.955C121.91 52.4182 121.875 57.4144 121.851 62.4106C121.821 68.5717 121.808 74.7338 121.81 80.8959C124.658 80.8726 127.506 80.8482 130.355 80.8249V86.8907L8.40549 86.8917Z" fill="#FF4F00"/>
|
||||
<path d="M124.912 40.1348H9.02975C4.08481 40.1348 0.0752715 36.3339 0.0752715 31.6439V8.49088C0.0752715 3.80189 4.08481 0 9.02975 0H130.282C130.264 1.47615 130.246 2.95331 130.229 4.42945C129.018 4.43249 127.808 4.43554 126.596 4.43858C125.745 8.73218 124.895 13.0268 124.045 17.3204C122.613 23.2077 121.182 29.0951 119.749 34.9814H124.935C124.927 36.6989 124.919 38.4173 124.912 40.1348Z" fill="#FF4F00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
133
src/desktop-header/DesktopHeader.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
// Local Components
|
||||
import DesktopUserMenuToggleSlot
|
||||
from '../plugin-slots/DesktopUserMenuToggleSlot';
|
||||
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||
import LogoSlot from '../plugin-slots/LogoSlot';
|
||||
import DesktopLoggedOutItemsSlot from '../plugin-slots/DesktopLoggedOutItemsSlot';
|
||||
import { desktopLoggedOutItemsDataShape } from './DesktopLoggedOutItems';
|
||||
import DesktopMainMenuSlot from '../plugin-slots/DesktopMainMenuSlot';
|
||||
import { desktopHeaderMainOrSecondaryMenuDataShape } from './DesktopHeaderMainOrSecondaryMenu';
|
||||
import DesktopSecondaryMenuSlot from '../plugin-slots/DesktopSecondaryMenuSlot';
|
||||
import DesktopUserMenuSlot from '../plugin-slots/DesktopUserMenuSlot';
|
||||
import { desktopUserMenuDataShape } from './DesktopHeaderUserMenu';
|
||||
|
||||
// i18n
|
||||
import messages from '../Header.messages';
|
||||
|
||||
// Assets
|
||||
import andalLogo from '../assets/complete-logo.svg';
|
||||
|
||||
const DesktopHeader = ({
|
||||
mainMenu,
|
||||
secondaryMenu,
|
||||
userMenu,
|
||||
loggedOutItems,
|
||||
logo,
|
||||
logoAltText,
|
||||
logoDestination,
|
||||
avatar,
|
||||
username,
|
||||
loggedIn,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const renderMainMenu = () => <DesktopMainMenuSlot menu={mainMenu} />;
|
||||
|
||||
const renderSecondaryMenu = () => <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
|
||||
|
||||
const renderUserMenu = () => (
|
||||
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
|
||||
<MenuTrigger
|
||||
tag="button"
|
||||
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
|
||||
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
|
||||
>
|
||||
<DesktopUserMenuToggleSlot avatar={avatar} label={username} />
|
||||
</MenuTrigger>
|
||||
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
|
||||
<DesktopUserMenuSlot menu={userMenu} />
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const renderLoggedOutItems = () => <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
|
||||
|
||||
// Use local Andal logo instead of passed logo prop
|
||||
const logoProps = { src: andalLogo, alt: 'Andal Learning', href: logoDestination || getConfig().LMS_BASE_URL };
|
||||
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
|
||||
|
||||
return (
|
||||
<header className="site-header-desktop">
|
||||
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
|
||||
<div className={`container-fluid ${logoClasses}`}>
|
||||
<div className="nav-container position-relative d-flex align-items-center">
|
||||
<LogoSlot {...logoProps} />
|
||||
<nav
|
||||
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
|
||||
className="nav main-nav"
|
||||
>
|
||||
{renderMainMenu()}
|
||||
</nav>
|
||||
<nav
|
||||
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
|
||||
className="nav secondary-menu-container align-items-center ml-auto"
|
||||
>
|
||||
{loggedIn
|
||||
? (
|
||||
<>
|
||||
{renderSecondaryMenu()}
|
||||
{renderUserMenu()}
|
||||
</>
|
||||
) : renderLoggedOutItems()}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export const desktopHeaderDataShape = {
|
||||
mainMenu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||
secondaryMenu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||
userMenu: desktopUserMenuDataShape,
|
||||
loggedOutItems: desktopLoggedOutItemsDataShape,
|
||||
logo: PropTypes.string,
|
||||
logoAltText: PropTypes.string,
|
||||
logoDestination: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
loggedIn: PropTypes.bool,
|
||||
};
|
||||
|
||||
DesktopHeader.propTypes = {
|
||||
mainMenu: desktopHeaderDataShape.mainMenu,
|
||||
secondaryMenu: desktopHeaderDataShape.secondaryMenu,
|
||||
userMenu: desktopHeaderDataShape.userMenu,
|
||||
loggedOutItems: desktopHeaderDataShape.loggedOutItems,
|
||||
logo: desktopHeaderDataShape.logo,
|
||||
logoAltText: desktopHeaderDataShape.logoAltText,
|
||||
logoDestination: desktopHeaderDataShape.logoDestination,
|
||||
avatar: desktopHeaderDataShape.avatar,
|
||||
username: desktopHeaderDataShape.username,
|
||||
loggedIn: desktopHeaderDataShape.loggedIn,
|
||||
};
|
||||
|
||||
DesktopHeader.defaultProps = {
|
||||
mainMenu: [],
|
||||
secondaryMenu: [],
|
||||
userMenu: [],
|
||||
loggedOutItems: [],
|
||||
logo: null,
|
||||
logoAltText: null,
|
||||
logoDestination: null,
|
||||
avatar: null,
|
||||
username: null,
|
||||
loggedIn: false,
|
||||
};
|
||||
|
||||
export default DesktopHeader;
|
||||
59
src/desktop-header/DesktopHeaderMainOrSecondaryMenu.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||
import { CaretIcon } from '../Icons';
|
||||
|
||||
const DesktopHeaderMainOrSecondaryMenu = ({ menu }) => {
|
||||
// Nodes are accepted as a prop
|
||||
if (!Array.isArray(menu)) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
return menu.map((menuItem) => {
|
||||
const {
|
||||
type,
|
||||
href,
|
||||
content,
|
||||
submenuContent,
|
||||
disabled,
|
||||
isActive,
|
||||
onClick,
|
||||
} = menuItem;
|
||||
|
||||
if (type === 'item') {
|
||||
return (
|
||||
<a
|
||||
key={`${type}-${content}`}
|
||||
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
||||
href={href}
|
||||
onClick={onClick || null}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
|
||||
<MenuTrigger onClick={onClick || null} tag="a" className="nav-link d-inline-flex align-items-center" href={href}>
|
||||
{content} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||
</MenuTrigger>
|
||||
<MenuContent className="pin-left pin-right shadow py-2">
|
||||
{submenuContent}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const desktopHeaderMainOrSecondaryMenuDataShape = PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]);
|
||||
|
||||
DesktopHeaderMainOrSecondaryMenu.propTypes = {
|
||||
menu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||
};
|
||||
|
||||
export default DesktopHeaderMainOrSecondaryMenu;
|
||||
39
src/desktop-header/DesktopHeaderUserMenu.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DesktopHeaderUserMenu = ({ menu }) => menu.map((group, index) => (
|
||||
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
|
||||
<React.Fragment key={index}>
|
||||
{group.heading && <div className="dropdown-header" role="heading" aria-level="1">{group.heading}</div>}
|
||||
{group.items.map(({
|
||||
type, content, href, disabled, isActive, onClick,
|
||||
}) => (
|
||||
<a
|
||||
className={`dropdown-${type}${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
||||
key={`${type}-${content}`}
|
||||
href={href}
|
||||
onClick={onClick || null}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
))}
|
||||
{index < menu.length - 1 && <div className="dropdown-divider" role="separator" />}
|
||||
</React.Fragment>
|
||||
));
|
||||
|
||||
export const desktopUserMenuDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||
heading: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
})),
|
||||
}));
|
||||
|
||||
DesktopHeaderUserMenu.propTypes = {
|
||||
menu: desktopUserMenuDataShape,
|
||||
};
|
||||
|
||||
export default DesktopHeaderUserMenu;
|
||||
24
src/desktop-header/DesktopLoggedOutItems.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DesktopLoggedOutItems = ({ items }) => items.map((item, i, arr) => (
|
||||
<a
|
||||
key={`${item.type}-${item.content}`}
|
||||
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
|
||||
href={item.href}
|
||||
>
|
||||
{item.content}
|
||||
</a>
|
||||
));
|
||||
|
||||
export const desktopLoggedOutItemsDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
}));
|
||||
|
||||
DesktopLoggedOutItems.propTypes = {
|
||||
items: desktopLoggedOutItemsDataShape,
|
||||
};
|
||||
|
||||
export default DesktopLoggedOutItems;
|
||||
20
src/desktop-header/DesktopUserMenuToggle.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CaretIcon } from '../Icons';
|
||||
import Avatar from '../Avatar';
|
||||
|
||||
const DesktopUserMenuToggle = ({ avatar, label }) => (
|
||||
<>
|
||||
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
|
||||
{label} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||
</>
|
||||
);
|
||||
|
||||
export const DesktopUserMenuTogglePropTypes = {
|
||||
avatar: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
DesktopUserMenuToggle.propTypes = DesktopUserMenuTogglePropTypes;
|
||||
|
||||
export default DesktopUserMenuToggle;
|
||||
41
src/frontend-platform.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// frontend-platform currently doesn't provide types... do it ourselves for i18n module at least.
|
||||
// We can remove this in the future when we migrate to frontend-shell, or when frontend-platform gets types
|
||||
// (whichever comes first).
|
||||
|
||||
declare module '@edx/frontend-platform/i18n' {
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { injectIntl as _injectIntl } from 'react-intl';
|
||||
/** @deprecated Use useIntl() hook instead. */
|
||||
export const injectIntl: typeof _injectIntl;
|
||||
/** @deprecated Use useIntl() hook instead. */
|
||||
export const intlShape: any;
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
export {
|
||||
createIntl,
|
||||
FormattedDate,
|
||||
FormattedTime,
|
||||
FormattedRelativeTime,
|
||||
FormattedNumber,
|
||||
FormattedPlural,
|
||||
FormattedMessage,
|
||||
defineMessages,
|
||||
IntlProvider,
|
||||
useIntl,
|
||||
} from 'react-intl';
|
||||
|
||||
// Other exports from the i18n module:
|
||||
export const configure: any;
|
||||
export const getPrimaryLanguageSubtag: (code: string) => string;
|
||||
export const getLocale: (locale?: string) => string;
|
||||
export const getMessages: any;
|
||||
export const isRtl: (locale?: string) => boolean;
|
||||
export const handleRtl: any;
|
||||
export const mergeMessages: any;
|
||||
export const LOCALE_CHANGED: any;
|
||||
export const LOCALE_TOPIC: any;
|
||||
export const getCountryList: any;
|
||||
export const getCountryMessages: any;
|
||||
export const getLanguageList: any;
|
||||
export const getLanguageMessages: any;
|
||||
}
|
||||
@@ -1,34 +1 @@
|
||||
import arMessages from './messages/ar.json';
|
||||
|
||||
import caMessages from './messages/ca.json';
|
||||
import heMessages from './messages/he.json';
|
||||
import idMessages from './messages/id.json';
|
||||
import plMessages from './messages/pl.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import thMessages from './messages/th.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import kokrMessages from './messages/ko_KR.json';
|
||||
import ptbrMessages from './messages/pt_BR.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
ca: caMessages,
|
||||
he: heMessages,
|
||||
id: idMessages,
|
||||
pl: plMessages,
|
||||
ru: ruMessages,
|
||||
th: thMessages,
|
||||
uk: ukMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
'ko-kr': kokrMessages,
|
||||
'pt-br': ptbrMessages,
|
||||
};
|
||||
|
||||
export default messages;
|
||||
export default {};
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import Header from './Header';
|
||||
import LearningHeader from './learning-header/LearningHeader';
|
||||
import messages from './i18n/index';
|
||||
import StudioHeader from './studio-header';
|
||||
|
||||
export { LearningHeader, messages };
|
||||
export { LearningHeader, messages, StudioHeader };
|
||||
|
||||
export default Header;
|
||||
|
||||
119
src/index.scss
@@ -1,8 +1,58 @@
|
||||
$spacer: 1rem;
|
||||
$blue: #007db8;
|
||||
$blue: #ff4f00;
|
||||
$white: #fff;
|
||||
$component-active-bg: #ff4f00 !default;
|
||||
$component-active-color: $white !default;
|
||||
$rounded-pill: 50rem !default;
|
||||
|
||||
// Andal Learning Brand Colors - Override Paragon variables
|
||||
$primary: #ff4f00;
|
||||
:root {
|
||||
--pgn-color-primary: #ff4f00;
|
||||
--pgn-color-primary-100: #ffe6cc;
|
||||
--pgn-color-primary-200: #ffcc99;
|
||||
--pgn-color-primary-300: #ffb366;
|
||||
--pgn-color-primary-400: #ff9933;
|
||||
--pgn-color-primary-500: #ff4f00;
|
||||
--pgn-color-primary-600: #cc3f00;
|
||||
--pgn-color-primary-700: #992f00;
|
||||
--pgn-color-primary-800: #661f00;
|
||||
--pgn-color-primary-900: #330f00;
|
||||
--pgn-color-action-primary: #ff4f00;
|
||||
--pgn-color-action-primary-hover: #cc3f00;
|
||||
--pgn-color-action-primary-focus: #992f00;
|
||||
--pgn-color-action-primary-active: #992f00;
|
||||
}
|
||||
|
||||
// Override all button variants to use Andal orange
|
||||
.btn-primary {
|
||||
background-color: #ff4f00 !important;
|
||||
border-color: #ff4f00 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #cc3f00 !important;
|
||||
border-color: #cc3f00 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #ff4f00 !important;
|
||||
border-color: #ff4f00 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #ff4f00 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-brand {
|
||||
background-color: #ff4f00 !important;
|
||||
border-color: #ff4f00 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
@import './Menu/menu.scss';
|
||||
@import './studio-header/StudioHeader.scss';
|
||||
|
||||
.dropdown-item a {
|
||||
text-decoration: none;
|
||||
@@ -20,14 +70,15 @@ $white: #fff;
|
||||
padding: .75rem;
|
||||
justify-content: center;
|
||||
align-items:center;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: rgba(0,0,0,.1);
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.learning-header {
|
||||
min-width: 0;
|
||||
|
||||
|
||||
.course-title-lockup {
|
||||
min-width: 0;
|
||||
|
||||
@@ -35,17 +86,12 @@ $white: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 0.1rem;
|
||||
padding-bottom: calc(var(--pgn-spacing-spacer-base, $spacer)* 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
.btn {
|
||||
height: 3rem;
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
.user-dropdown .btn {
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +108,7 @@ $white: #fff;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 1.5rem;
|
||||
}
|
||||
@@ -69,52 +116,68 @@ $white: #fff;
|
||||
|
||||
|
||||
.site-header-desktop {
|
||||
box-shadow: 0 1px 0 0 rgba(0,0,0,.1);
|
||||
background: $white;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, .1);
|
||||
background: var(--pgn-color-white, $white);
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
position: relative;
|
||||
top: -.05em;
|
||||
height: 1.75rem;
|
||||
padding: 1rem 0;
|
||||
margin-right: 1rem;
|
||||
padding: var(--pgn-spacing-spacer-base, $spacer) 0;
|
||||
margin-right: var(--pgn-spacing-spacer-base, $spacer);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.main-nav {
|
||||
.nav-link {
|
||||
padding: 1.125rem 1rem;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
.secondary-menu-container {
|
||||
.nav-link:hover,
|
||||
.nav-link:focus,
|
||||
.nav-link.active,
|
||||
.expanded .nav-link {
|
||||
background: $component-active-bg;
|
||||
color: $component-active-color;
|
||||
background: var(--pgn-color-bg-active, $component-active-bg);
|
||||
color: var(--pgn-color-active, $component-active-color);
|
||||
}
|
||||
}
|
||||
.main-nav {
|
||||
.nav-link {
|
||||
padding: 1.125rem var(--pgn-spacing-spacer-base, $spacer);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:focus,
|
||||
.nav-link.active,
|
||||
.expanded .nav-link {
|
||||
background: var(--pgn-color-bg-active, $component-active-bg);
|
||||
color: var(--pgn-color-active, $component-active-color);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: static;
|
||||
|
||||
.menu-content {
|
||||
border-top: solid 2px $component-active-bg;
|
||||
border-top: solid 2px var(--pgn-color-bg-active);
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.25);
|
||||
box-shadow: var(--pgn-elevation-box-shadow-down-1, 0 1px 2px rgba(0,0,0,.25));
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
padding: 1rem;
|
||||
padding: var(--pgn-spacing-spacer-base, $spacer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border-radius: $rounded-pill;
|
||||
border-radius: var(--pgn-size-rounded-pill, $rounded-pill);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,26 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import LearningLoggedOutItemsSlot from '../plugin-slots/LearningLoggedOutItemsSlot';
|
||||
|
||||
import genericMessages from '../generic/messages';
|
||||
|
||||
function AnonymousUserMenu({ intl }) {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="mr-3"
|
||||
variant="outline-primary"
|
||||
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.registerSentenceCase)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={`${getLoginRedirectUrl(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const AnonymousUserMenu = () => {
|
||||
const intl = useIntl();
|
||||
const buttonsInfo = [
|
||||
{
|
||||
message: intl.formatMessage(genericMessages.registerSentenceCase),
|
||||
href: `${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`,
|
||||
},
|
||||
{
|
||||
message: intl.formatMessage(genericMessages.signInSentenceCase),
|
||||
href: getLoginRedirectUrl(global.location.href),
|
||||
variant: 'primary',
|
||||
},
|
||||
];
|
||||
|
||||
AnonymousUserMenu.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
return <LearningLoggedOutItemsSlot buttonsInfo={buttonsInfo} />;
|
||||
};
|
||||
|
||||
export default injectIntl(AnonymousUserMenu);
|
||||
export default AnonymousUserMenu;
|
||||
|
||||
@@ -1,57 +1,55 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
|
||||
import LearningUserMenuToggleSlot from '../plugin-slots/LearningUserMenuToggleSlot';
|
||||
import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function AuthenticatedUserDropdown({ intl, username }) {
|
||||
const dashboardMenuItem = (
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{intl.formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
const AuthenticatedUserDropdown = ({ username }) => {
|
||||
const intl = useIntl();
|
||||
const dropdownItems = [
|
||||
{
|
||||
message: intl.formatMessage(messages.dashboard),
|
||||
href: `${getConfig().LMS_BASE_URL}/dashboard`,
|
||||
},
|
||||
{
|
||||
message: intl.formatMessage(messages.profile),
|
||||
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`,
|
||||
},
|
||||
{
|
||||
message: intl.formatMessage(messages.account),
|
||||
href: getConfig().ACCOUNT_SETTINGS_URL,
|
||||
},
|
||||
...(getConfig().ORDER_HISTORY_URL ? [{
|
||||
message: intl.formatMessage(messages.orderHistory),
|
||||
href: getConfig().ORDER_HISTORY_URL,
|
||||
}] : []),
|
||||
{
|
||||
message: intl.formatMessage(messages.signOut),
|
||||
href: getConfig().LOGOUT_URL,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle variant="outline-primary">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{dashboardMenuItem}
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
||||
{intl.formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{intl.formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{ getConfig().ORDER_HISTORY_URL && (
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{intl.formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{intl.formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</>
|
||||
<Dropdown className="user-dropdown ml-3">
|
||||
<Dropdown.Toggle variant="outline-primary" aria-label={intl.formatMessage(messages.userOptionsDropdownLabel)}>
|
||||
<LearningUserMenuToggleSlot label={username} icon={faUserCircle} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<LearningUserMenuSlot items={dropdownItems} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthenticatedUserDropdown);
|
||||
export default AuthenticatedUserDropdown;
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
import LogoSlot from '../plugin-slots/LogoSlot';
|
||||
import CourseInfoSlot from '../plugin-slots/CourseInfoSlot';
|
||||
import { courseInfoDataShape } from './LearningHeaderCourseInfo';
|
||||
import messages from './messages';
|
||||
import LearningHelpSlot from '../plugin-slots/LearningHelpSlot';
|
||||
|
||||
function LinkedLogo({
|
||||
href,
|
||||
src,
|
||||
alt,
|
||||
...attributes
|
||||
}) {
|
||||
return (
|
||||
<a href={href} {...attributes}>
|
||||
<img className="d-block" src={src} alt={alt} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
LinkedLogo.propTypes = {
|
||||
href: PropTypes.string.isRequired,
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function LearningHeader({
|
||||
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
|
||||
}) {
|
||||
const LearningHeader = ({
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
showUserDropdown,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
|
||||
const headerLogo = (
|
||||
<LinkedLogo
|
||||
className="logo"
|
||||
<LogoSlot
|
||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
src={getConfig().LOGO_URL}
|
||||
alt={getConfig().SITE_NAME}
|
||||
@@ -46,28 +34,29 @@ function LearningHeader({
|
||||
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
||||
<div className="container-xl py-2 d-flex align-items-center">
|
||||
{headerLogo}
|
||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||
<div className="flex-grow-1 course-title-lockup d-flex" style={{ lineHeight: 1 }}>
|
||||
<CourseInfoSlot courseOrg={courseOrg} courseNumber={courseNumber} courseTitle={courseTitle} />
|
||||
</div>
|
||||
{showUserDropdown && authenticatedUser && (
|
||||
<>
|
||||
<LearningHelpSlot />
|
||||
<AuthenticatedUserDropdown
|
||||
username={authenticatedUser.username}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showUserDropdown && !authenticatedUser && (
|
||||
<AnonymousUserMenu />
|
||||
<AnonymousUserMenu />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LearningHeader.propTypes = {
|
||||
courseOrg: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
courseTitle: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
courseOrg: courseInfoDataShape.courseOrg,
|
||||
courseNumber: courseInfoDataShape.courseNumber,
|
||||
courseTitle: courseInfoDataShape.courseTitle,
|
||||
showUserDropdown: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -78,4 +67,4 @@ LearningHeader.defaultProps = {
|
||||
showUserDropdown: true,
|
||||
};
|
||||
|
||||
export default injectIntl(LearningHeader);
|
||||
export default LearningHeader;
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('Header', () => {
|
||||
|
||||
it('displays user button', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username);
|
||||
expect(screen.getByText(authenticatedUser.username)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays course data', () => {
|
||||
|
||||
23
src/learning-header/LearningHeaderCourseInfo.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LearningHeaderCourseInfo = ({
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
}) => (
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const courseInfoDataShape = {
|
||||
courseOrg: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
courseTitle: PropTypes.string,
|
||||
};
|
||||
|
||||
LearningHeaderCourseInfo.propTypes = courseInfoDataShape;
|
||||
|
||||
export default LearningHeaderCourseInfo;
|
||||
14
src/learning-header/LearningHeaderHelpLink.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const LearningHeaderHelpLink = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default LearningHeaderHelpLink;
|
||||
21
src/learning-header/LearningHeaderUserMenuItems.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
|
||||
const LearningHeaderUserMenuItems = ({ items }) => items.map((item) => (
|
||||
<Dropdown.Item href={item.href}>
|
||||
{item.message}
|
||||
</Dropdown.Item>
|
||||
));
|
||||
|
||||
export const learningHeaderUserMenuDataShape = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
LearningHeaderUserMenuItems.propTypes = learningHeaderUserMenuDataShape;
|
||||
|
||||
export default LearningHeaderUserMenuItems;
|
||||
26
src/learning-header/LearningLoggedOutButtons.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
const LearningLoggedOutButtons = ({ buttonsInfo }) => buttonsInfo.map(buttonInfo => (
|
||||
<Button
|
||||
className="ml-3"
|
||||
variant={buttonInfo.variant ?? 'outline-primary'}
|
||||
href={buttonInfo.href}
|
||||
>
|
||||
{buttonInfo.message}
|
||||
</Button>
|
||||
));
|
||||
|
||||
export const learningHeaderLoggedOutItemsDataShape = {
|
||||
buttonsInfo: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
variant: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
LearningLoggedOutButtons.propTypes = learningHeaderLoggedOutItemsDataShape;
|
||||
|
||||
export default LearningLoggedOutButtons;
|
||||
28
src/learning-header/LearningUserMenuToggle.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LearningUserMenuToggle = ({
|
||||
label,
|
||||
icon,
|
||||
}) => (
|
||||
<>
|
||||
<FontAwesomeIcon icon={icon} className="d-md-none" size="lg" />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{label}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
export const LearningUserMenuTogglePropTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
// Full shape available by examining @fortawesome/fontawesome-common-types/index.d.ts.
|
||||
icon: PropTypes.shape({
|
||||
prefix: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
LearningUserMenuToggle.propTypes = LearningUserMenuTogglePropTypes;
|
||||
|
||||
export default LearningUserMenuToggle;
|
||||
@@ -36,6 +36,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Sign Out',
|
||||
description: 'The label for the user menu Sign Out action.',
|
||||
},
|
||||
userOptionsDropdownLabel: {
|
||||
id: 'header.menu.aria-label',
|
||||
defaultMessage: 'User Options',
|
||||
description: 'The aria-label for the user options dropdown.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
147
src/mobile-header/MobileHeader.jsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
// Local Components
|
||||
import MobileUserMenuToggleSlot from '../plugin-slots/MobileUserMenuToggleSlot';
|
||||
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||
import LogoSlot from '../plugin-slots/LogoSlot';
|
||||
import MobileLoggedOutItemsSlot from '../plugin-slots/MobileLoggedOutItemsSlot';
|
||||
import { mobileHeaderLoggedOutItemsDataShape } from './MobileLoggedOutItems';
|
||||
import MobileMainMenuSlot from '../plugin-slots/MobileMainMenuSlot';
|
||||
import { mobileHeaderMainMenuDataShape } from './MobileHeaderMainMenu';
|
||||
import MobileUserMenuSlot from '../plugin-slots/MobileUserMenuSlot';
|
||||
import { mobileHeaderUserMenuDataShape } from './MobileHeaderUserMenu';
|
||||
|
||||
// i18n
|
||||
import messages from '../Header.messages';
|
||||
|
||||
// Assets
|
||||
import andalLogo from '../assets/complete-logo.svg';
|
||||
import { MenuIcon } from '../Icons';
|
||||
|
||||
const MobileHeader = ({
|
||||
mainMenu,
|
||||
secondaryMenu,
|
||||
userMenu,
|
||||
loggedOutItems,
|
||||
logo,
|
||||
logoAltText,
|
||||
logoDestination,
|
||||
avatar,
|
||||
username,
|
||||
loggedIn,
|
||||
stickyOnMobile,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const renderMainMenu = () => <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
|
||||
|
||||
const renderUserMenuItems = () => <MobileUserMenuSlot menu={userMenu} />;
|
||||
|
||||
const renderLoggedOutItems = () => <MobileLoggedOutItemsSlot items={loggedOutItems} />;
|
||||
|
||||
const renderUserMenuToggle = () => <MobileUserMenuToggleSlot avatar={avatar} label={username} />;
|
||||
|
||||
// Use local Andal logo instead of passed logo prop
|
||||
const logoProps = { src: andalLogo, alt: 'Andal Learning', href: logoDestination };
|
||||
const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
|
||||
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'justify-content-left pl-3' : 'justify-content-center';
|
||||
|
||||
return (
|
||||
<header
|
||||
aria-label={intl.formatMessage(messages['header.label.main.header'])}
|
||||
className={`site-header-mobile d-flex justify-content-between align-items-center shadow ${stickyClassName}`}
|
||||
>
|
||||
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
|
||||
{mainMenu.length > 0 ? (
|
||||
<div className="w-100 d-flex justify-content-start">
|
||||
|
||||
<Menu className="position-static">
|
||||
<MenuTrigger
|
||||
tag="button"
|
||||
className="icon-button"
|
||||
aria-label={intl.formatMessage(messages['header.label.main.menu'])}
|
||||
title={intl.formatMessage(messages['header.label.main.menu'])}
|
||||
>
|
||||
<MenuIcon role="img" aria-hidden focusable="false" style={{ width: '1.5rem', height: '1.5rem' }} />
|
||||
</MenuTrigger>
|
||||
<MenuContent
|
||||
tag="nav"
|
||||
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
|
||||
className="nav flex-column pin-left pin-right border-top shadow py-2"
|
||||
>
|
||||
{renderMainMenu()}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`w-100 d-flex ${logoClasses}`}>
|
||||
<LogoSlot {...logoProps} itemType="http://schema.org/Organization" />
|
||||
</div>
|
||||
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
|
||||
<div className="w-100 d-flex justify-content-end align-items-center">
|
||||
<Menu tag="nav" aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} className="position-static">
|
||||
<MenuTrigger
|
||||
tag="button"
|
||||
className="icon-button"
|
||||
aria-label={intl.formatMessage(messages['header.label.account.menu'])}
|
||||
title={intl.formatMessage(messages['header.label.account.menu'])}
|
||||
>
|
||||
{renderUserMenuToggle()}
|
||||
</MenuTrigger>
|
||||
<MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2">
|
||||
{loggedIn ? renderUserMenuItems() : renderLoggedOutItems()}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export const mobileHeaderDataShape = {
|
||||
mainMenu: mobileHeaderMainMenuDataShape,
|
||||
secondaryMenu: mobileHeaderMainMenuDataShape,
|
||||
userMenu: mobileHeaderUserMenuDataShape,
|
||||
loggedOutItems: mobileHeaderLoggedOutItemsDataShape,
|
||||
logo: PropTypes.string,
|
||||
logoAltText: PropTypes.string,
|
||||
logoDestination: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
loggedIn: PropTypes.bool,
|
||||
stickyOnMobile: PropTypes.bool,
|
||||
};
|
||||
|
||||
MobileHeader.propTypes = {
|
||||
mainMenu: mobileHeaderDataShape.mainMenu,
|
||||
secondaryMenu: mobileHeaderDataShape.secondaryMenu,
|
||||
userMenu: mobileHeaderDataShape.userMenu,
|
||||
loggedOutItems: mobileHeaderDataShape.loggedOutItems,
|
||||
logo: mobileHeaderDataShape.logo,
|
||||
logoAltText: mobileHeaderDataShape.logoAltText,
|
||||
logoDestination: mobileHeaderDataShape.logoDestination,
|
||||
avatar: mobileHeaderDataShape.avatar,
|
||||
username: mobileHeaderDataShape.username,
|
||||
loggedIn: mobileHeaderDataShape.loggedIn,
|
||||
stickyOnMobile: mobileHeaderDataShape.stickyOnMobile,
|
||||
};
|
||||
|
||||
MobileHeader.defaultProps = {
|
||||
mainMenu: [],
|
||||
secondaryMenu: [],
|
||||
userMenu: [],
|
||||
loggedOutItems: [],
|
||||
logo: null,
|
||||
logoAltText: null,
|
||||
logoDestination: null,
|
||||
avatar: null,
|
||||
username: null,
|
||||
loggedIn: false,
|
||||
stickyOnMobile: true,
|
||||
|
||||
};
|
||||
|
||||
export default MobileHeader;
|
||||
58
src/mobile-header/MobileHeaderMainMenu.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||
|
||||
const MobileHeaderMainMenu = ({ menu }) => {
|
||||
// Nodes are accepted as a prop
|
||||
if (!Array.isArray(menu)) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
return menu.map((menuItem) => {
|
||||
const {
|
||||
type,
|
||||
href,
|
||||
content,
|
||||
submenuContent,
|
||||
disabled,
|
||||
isActive,
|
||||
onClick,
|
||||
} = menuItem;
|
||||
|
||||
if (type === 'item') {
|
||||
return (
|
||||
<a
|
||||
key={`${type}-${content}`}
|
||||
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
||||
href={href}
|
||||
onClick={onClick || null}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
|
||||
<MenuTrigger onClick={onClick || null} tag="a" role="button" tabIndex="0" className="nav-link">
|
||||
{content}
|
||||
</MenuTrigger>
|
||||
<MenuContent className="position-static pin-left pin-right py-2">
|
||||
{submenuContent}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const mobileHeaderMainMenuDataShape = PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]);
|
||||
|
||||
MobileHeaderMainMenu.propTypes = {
|
||||
menu: mobileHeaderMainMenuDataShape,
|
||||
};
|
||||
|
||||
export default MobileHeaderMainMenu;
|
||||
35
src/mobile-header/MobileHeaderUserMenu.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MobileHeaderUserMenu = ({ menu }) => menu.map((group) => (
|
||||
group.items.map(({
|
||||
type, content, href, disabled, isActive, onClick,
|
||||
}) => (
|
||||
<li className="nav-item" key={`${type}-${content}`}>
|
||||
<a
|
||||
className={`nav-link${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
||||
href={href}
|
||||
onClick={onClick || null}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
));
|
||||
|
||||
export const mobileHeaderUserMenuDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||
heading: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
})),
|
||||
}));
|
||||
|
||||
MobileHeaderUserMenu.propTypes = {
|
||||
menu: mobileHeaderUserMenuDataShape,
|
||||
};
|
||||
|
||||
export default MobileHeaderUserMenu;
|
||||
25
src/mobile-header/MobileLoggedOutItems.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MobileLoggedOutItems = ({ items }) => items.map(({ type, href, content }, i, arr) => (
|
||||
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
|
||||
<a
|
||||
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
|
||||
href={href}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
</li>
|
||||
));
|
||||
|
||||
export const mobileHeaderLoggedOutItemsDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
}));
|
||||
|
||||
MobileLoggedOutItems.propTypes = {
|
||||
menu: mobileHeaderLoggedOutItemsDataShape,
|
||||
};
|
||||
|
||||
export default MobileLoggedOutItems;
|
||||
14
src/mobile-header/MobileUserMenuToggle.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from '../Avatar';
|
||||
|
||||
const MobileUserMenuToggle = ({ avatar, username }) => <Avatar size="1.5rem" src={avatar} alt={username} />;
|
||||
|
||||
export const MobileUserMenuTogglePropTypes = {
|
||||
avatar: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
MobileUserMenuToggle.propTypes = MobileUserMenuTogglePropTypes;
|
||||
|
||||
export default MobileUserMenuToggle;
|
||||
128
src/plugin-slots/CourseInfoSlot/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Course Info Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.layout.header_learning_course_info.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `course_info_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the course info.
|
||||
|
||||
## Examples
|
||||
|
||||
### Replace Course Title
|
||||
|
||||
The following `env.config.jsx` will replace the course title.
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const replaceCourseTitle = ( widget ) => {
|
||||
widget.content.courseTitle = "Custom Course Title";
|
||||
return widget;
|
||||
};
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_learning_course_info.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Modify,
|
||||
widgetId: 'default_contents',
|
||||
fn: replaceCourseTitle,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Replace Course Info with Custom Component
|
||||
|
||||
The following `env.config.jsx` will replace the course info entirely (in this case with a centered 🗺️ `h1`)
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_learning_course_info.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_course_info_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Add Custom Components before and after Course Info
|
||||
|
||||
The following `env.config.jsx` will place custom components before and after the course info (in this case centered `h1`s with 🌜 and 🌛).
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_learning_course_info.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_before_course_info_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 10,
|
||||
RenderWidget: () => (
|
||||
<h3 style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: '0.5rem',
|
||||
}}>🌜</h3>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_after_course_info_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 90,
|
||||
RenderWidget: () => (
|
||||
<h3 style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginLeft: '0.5rem',
|
||||
}}>🌛</h3>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
BIN
src/plugin-slots/CourseInfoSlot/images/replace_course_title.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
34
src/plugin-slots/CourseInfoSlot/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import LearningHeaderCourseInfo, { courseInfoDataShape } from '../../learning-header/LearningHeaderCourseInfo';
|
||||
|
||||
const CourseInfoSlot = ({
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
...attributes
|
||||
}) => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.layout.header_learning_course_info.v1"
|
||||
idAliases={['course_info_slot']}
|
||||
slotOptions={{
|
||||
mergeProps: true,
|
||||
}}
|
||||
pluginProps={{
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
}}
|
||||
>
|
||||
<LearningHeaderCourseInfo
|
||||
courseOrg={courseOrg}
|
||||
courseNumber={courseNumber}
|
||||
courseTitle={courseTitle}
|
||||
{...attributes}
|
||||
/>
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
CourseInfoSlot.propTypes = courseInfoDataShape;
|
||||
|
||||
export default CourseInfoSlot;
|
||||
44
src/plugin-slots/DesktopHeaderSlot/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Desktop Header Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.layout.header_desktop.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `desktop_header_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the entire desktop header.
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Component
|
||||
|
||||
The following `env.config.jsx` will replace the desktop header entirely (in this case with a centered 🗺️ `h1`)
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_desktop_header_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
After Width: | Height: | Size: 27 KiB |
21
src/plugin-slots/DesktopHeaderSlot/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import DesktopHeader, { desktopHeaderDataShape } from '../../desktop-header/DesktopHeader';
|
||||
|
||||
const DesktopHeaderSlot = ({
|
||||
props,
|
||||
}) => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.layout.header_desktop.v1"
|
||||
idAliases={['desktop_header_slot']}
|
||||
slotOptions={{
|
||||
mergeProps: true,
|
||||
}}
|
||||
>
|
||||
<DesktopHeader {...props} />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
DesktopHeaderSlot.propTypes = desktopHeaderDataShape;
|
||||
|
||||
export default DesktopHeaderSlot;
|
||||
137
src/plugin-slots/DesktopLoggedOutItemsSlot/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Desktop Logged Out Items Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.layout.header_desktop_logged_out_items.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `desktop_logged_out_items_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the items shown on desktop when the user is logged out.
|
||||
|
||||
## Examples
|
||||
|
||||
### Modify Items
|
||||
|
||||
The following `env.config.jsx` will modify the items shown on desktop when the user is logged out.
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const modifyLoggedOutItems = ( widget ) => {
|
||||
widget.content.items = [
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://openedx.org/',
|
||||
content: 'openedx.org',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://docs.openedx.org/en/latest/',
|
||||
content: 'Documentation',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://discuss.openedx.org/',
|
||||
content: 'Forums',
|
||||
}
|
||||
];
|
||||
return widget;
|
||||
};
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Modify,
|
||||
widgetId: 'default_contents',
|
||||
fn: modifyLoggedOutItems,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Replace with Custom Component
|
||||
|
||||
The following `env.config.jsx` will replace the items shown on desktop when the user is logged out entirely (in this case with a centered 🗺️ `h1`)
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_logged_out_items_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Add Custom Components before and after
|
||||
|
||||
The following `env.config.jsx` will place custom components before and after the items shown on desktop when the user is logged out (in this case centered `h1`s with 🌜 and 🌛).
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_before_logged_out_items_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 10,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_after_logged_out_items_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 90,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
23
src/plugin-slots/DesktopLoggedOutItemsSlot/index.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import DesktopLoggedOutItems, { desktopLoggedOutItemsDataShape } from '../../desktop-header/DesktopLoggedOutItems';
|
||||
|
||||
const DesktopLoggedOutItemsSlot = ({
|
||||
items,
|
||||
}) => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.layout.header_desktop_logged_out_items.v1"
|
||||
idAliases={['desktop_logged_out_items_slot']}
|
||||
slotOptions={{
|
||||
mergeProps: true,
|
||||
}}
|
||||
>
|
||||
<DesktopLoggedOutItems items={items} />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
DesktopLoggedOutItemsSlot.propTypes = {
|
||||
items: desktopLoggedOutItemsDataShape,
|
||||
};
|
||||
|
||||
export default DesktopLoggedOutItemsSlot;
|
||||
190
src/plugin-slots/DesktopMainMenuSlot/README.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Desktop Main Menu Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.layout.header_desktop_main_menu.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `desktop_main_menu_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the desktop main menu.
|
||||
|
||||
## Examples
|
||||
|
||||
### Modify Items
|
||||
|
||||
#### Replace All Items
|
||||
|
||||
The following `env.config.jsx` will replace all items in the desktop main menu.
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const modifyMainMenu = ( widget ) => {
|
||||
widget.content.menu = [
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://openedx.org/',
|
||||
content: 'openedx.org',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://docs.openedx.org/en/latest/',
|
||||
content: 'Documentation',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://discuss.openedx.org/',
|
||||
content: 'Forums',
|
||||
}
|
||||
];
|
||||
return widget;
|
||||
};
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Modify,
|
||||
widgetId: 'default_contents',
|
||||
fn: modifyMainMenu,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
#### Add Items
|
||||
|
||||
The following `env.config.jsx` will add items in the desktop main menu.
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const modifyMainMenu = (widget) => {
|
||||
const existingMenu = widget.RenderWidget.props.menu || [];
|
||||
|
||||
const newMarketingLinks = [
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://example.com/how-it-works',
|
||||
content: 'How it works',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://example.com/courses',
|
||||
content: 'Courses',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://example.com/schools',
|
||||
content: 'Schools',
|
||||
}
|
||||
];
|
||||
|
||||
widget.content.menu = [...existingMenu, ...newMarketingLinks];
|
||||
return widget;
|
||||
};
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Modify,
|
||||
widgetId: 'default_contents',
|
||||
fn: modifyMainMenu,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Replace Menu with Custom Component
|
||||
|
||||
The following `env.config.jsx` will replace the desktop main menu entirely (in this case with a centered 🗺️ `h1`)
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_main_menu_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Add Custom Components before and after Menu
|
||||
|
||||
The following `env.config.jsx` will place custom components before and after the desktop main menu (in this case centered `h1`s with 🌜 and 🌛).
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_before_main_menu_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 10,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_after_main_menu_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 90,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
23
src/plugin-slots/DesktopMainMenuSlot/index.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import DesktopHeaderMainOrSecondaryMenu, { desktopHeaderMainOrSecondaryMenuDataShape } from '../../desktop-header/DesktopHeaderMainOrSecondaryMenu';
|
||||
|
||||
const DesktopMainMenuSlot = ({
|
||||
menu,
|
||||
}) => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.layout.header_desktop_main_menu.v1"
|
||||
idAliases={['desktop_main_menu_slot']}
|
||||
slotOptions={{
|
||||
mergeProps: true,
|
||||
}}
|
||||
>
|
||||
<DesktopHeaderMainOrSecondaryMenu menu={menu} />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
DesktopMainMenuSlot.propTypes = {
|
||||
menu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||
};
|
||||
|
||||
export default DesktopMainMenuSlot;
|
||||
132
src/plugin-slots/DesktopSecondaryMenuSlot/README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Desktop Secondary Menu Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.layout.header_desktop_secondary_menu.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `desktop_secondary_menu_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the desktop secondary menu.
|
||||
|
||||
## Examples
|
||||
|
||||
### Modify Items
|
||||
|
||||
The following `env.config.jsx` will modify the items in the desktop secondary menu.
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const modifySecondaryMenu = ( widget ) => {
|
||||
widget.content.menu = [
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://www.youtube.com/c/openedx',
|
||||
content: 'Open edX on YouTube',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: 'https://github.com/openedx/',
|
||||
content: 'Open edX on GitHub',
|
||||
}
|
||||
];
|
||||
return widget;
|
||||
};
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Modify,
|
||||
widgetId: 'default_contents',
|
||||
fn: modifySecondaryMenu,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Replace Menu with Custom Component
|
||||
|
||||
The following `env.config.jsx` will replace the desktop secondary menu entirely (in this case with a centered 🗺️ `h1`)
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_secondary_menu_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Add Custom Components before and after Menu
|
||||
|
||||
The following `env.config.jsx` will place custom components before and after the desktop secondary menu (in this case centered `h1`s with 🌜 and 🌛).
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
|
||||
keepDefault: true,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_before_secondary_menu_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 10,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_after_secondary_menu_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 90,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||