pxt-core 7.4.11 → 7.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/built/cli.js +117 -83
- package/built/nodeutil.d.ts +1 -1
- package/built/nodeutil.js +2 -2
- package/built/pxt.js +119 -84
- package/built/pxtblockly.js +153 -60
- package/built/pxtblocks.d.ts +8 -0
- package/built/pxtblocks.js +96 -60
- package/built/pxtlib.js +2 -1
- package/built/target.js +1 -1
- package/built/web/blockly.css +1 -1
- package/built/web/main.js +1 -1
- package/built/web/pxtapp.js +1 -1
- package/built/web/pxtasseteditor.js +1 -1
- package/built/web/pxtblockly.js +53 -1
- package/built/web/pxtblocks.js +1 -1
- package/built/web/pxtembed.js +53 -1
- package/built/web/pxtlib.js +1 -1
- package/built/web/pxtworker.js +1 -1
- package/built/web/react-common-skillmap.css +13 -0
- package/built/web/rtlblockly.css +1 -1
- package/built/web/rtlreact-common-skillmap.css +13 -0
- package/built/web/rtlsemantic.css +14 -2
- package/built/web/semantic.css +14 -2
- package/built/web/skillmap/css/main.b2b69d60.chunk.css +1 -0
- package/built/web/skillmap/js/2.fce3190c.chunk.js +2 -0
- package/built/web/skillmap/js/main.9d64b2d7.chunk.js +1 -0
- package/docfiles/tracking.html +1 -1
- package/localtypings/pxtarget.d.ts +1 -0
- package/localtypings/pxtblockly.d.ts +37 -0
- package/package.json +8 -4
- package/react-common/components/Notification.tsx +82 -0
- package/react-common/components/controls/Button.tsx +63 -0
- package/react-common/components/controls/Checkbox.tsx +47 -0
- package/react-common/components/controls/Input.tsx +117 -0
- package/react-common/components/controls/List.tsx +28 -0
- package/react-common/components/controls/Modal.tsx +143 -0
- package/react-common/components/profile/Badge.tsx +33 -0
- package/react-common/components/profile/BadgeInfo.tsx +74 -0
- package/react-common/components/profile/BadgeList.tsx +67 -0
- package/react-common/components/profile/Profile.tsx +42 -0
- package/react-common/components/profile/UserNotification.tsx +32 -0
- package/react-common/components/profile/UserPane.tsx +68 -0
- package/react-common/components/types.d.ts +29 -0
- package/react-common/components/util.tsx +61 -0
- package/react-common/styles/controls/Button.less +174 -0
- package/react-common/styles/controls/Checkbox.less +13 -0
- package/react-common/styles/controls/Icon.less +11 -0
- package/react-common/styles/controls/Input.less +95 -0
- package/react-common/styles/controls/List.less +12 -0
- package/react-common/styles/controls/Modal.less +105 -0
- package/react-common/styles/controls/Spinner.less +24 -0
- package/{built/web/react-common.css → react-common/styles/profile/profile.less} +13 -0
- package/react-common/styles/react-common-skillmap-core.less +10 -0
- package/react-common/styles/react-common-skillmap.less +12 -0
- package/react-common/styles/react-common-variables.less +47 -0
- package/react-common/styles/react-common.less +12 -0
- package/react-common/tsconfig.json +36 -0
- package/theme/asset-editor.less +13 -29
- package/theme/blockly-core.less +16 -0
- package/theme/common-components.less +7 -0
- package/theme/common.less +1 -1
- package/theme/highcontrast.less +4 -0
- package/theme/pxt.less +2 -0
- package/theme/tutorial-sidebar.less +64 -6
- package/webapp/public/blockly/plugins.js +57 -0
- package/webapp/public/skillmap.html +3 -3
- package/built/web/skillmap/css/main.96b1b3f1.chunk.css +0 -1
- package/built/web/skillmap/js/2.7dd06a3a.chunk.js +0 -2
- package/built/web/skillmap/js/main.55881627.chunk.js +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { classList, ControlProps } from "../util";
|
|
3
|
+
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
|
|
6
|
+
export interface InputProps extends ControlProps {
|
|
7
|
+
initialValue?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
iconTitle?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
type?: string;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
autoComplete?: boolean;
|
|
17
|
+
selectOnClick?: boolean;
|
|
18
|
+
|
|
19
|
+
onChange?: (newValue: string) => void;
|
|
20
|
+
onEnterKey?: (value: string) => void;
|
|
21
|
+
onIconClick?: (value: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Input(props: InputProps) {
|
|
25
|
+
const {
|
|
26
|
+
id,
|
|
27
|
+
className,
|
|
28
|
+
role,
|
|
29
|
+
ariaHidden,
|
|
30
|
+
ariaLabel,
|
|
31
|
+
initialValue,
|
|
32
|
+
label,
|
|
33
|
+
title,
|
|
34
|
+
placeholder,
|
|
35
|
+
icon,
|
|
36
|
+
iconTitle,
|
|
37
|
+
disabled,
|
|
38
|
+
type,
|
|
39
|
+
readOnly,
|
|
40
|
+
autoComplete,
|
|
41
|
+
selectOnClick,
|
|
42
|
+
onChange,
|
|
43
|
+
onEnterKey,
|
|
44
|
+
onIconClick
|
|
45
|
+
} = props;
|
|
46
|
+
|
|
47
|
+
const [value, setValue] = React.useState(initialValue || "");
|
|
48
|
+
|
|
49
|
+
const clickHandler = (evt: React.MouseEvent<any>) => {
|
|
50
|
+
if (selectOnClick) {
|
|
51
|
+
(evt.target as any).select()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const changeHandler = (e: React.ChangeEvent<any>) => {
|
|
56
|
+
const newValue = (e.target as any).value;
|
|
57
|
+
if (!readOnly && (value !== newValue)) {
|
|
58
|
+
setValue(newValue);
|
|
59
|
+
}
|
|
60
|
+
if (onChange) {
|
|
61
|
+
onChange(newValue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const enterKeyHandler = (e: React.KeyboardEvent) => {
|
|
66
|
+
const charCode = (typeof e.which == "number") ? e.which : e.keyCode;
|
|
67
|
+
if (charCode === /*enter*/13 || charCode === /*space*/32) {
|
|
68
|
+
if (onEnterKey) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
onEnterKey(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const iconClickHandler = () => {
|
|
76
|
+
if (onIconClick) onIconClick(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={classList("common-input-wrapper", disabled && "disabled", className)}>
|
|
81
|
+
{label && <label className="common-input-label">
|
|
82
|
+
{label}
|
|
83
|
+
</label>}
|
|
84
|
+
<div className="common-input-group">
|
|
85
|
+
<input
|
|
86
|
+
id={id}
|
|
87
|
+
className={classList("common-input", icon && "has-icon")}
|
|
88
|
+
title={title}
|
|
89
|
+
role={role || "button"}
|
|
90
|
+
tabIndex={disabled ? 0 : -1}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
aria-hidden={ariaHidden}
|
|
93
|
+
type={type || "text"}
|
|
94
|
+
placeholder={placeholder}
|
|
95
|
+
value={value || ''}
|
|
96
|
+
readOnly={!!readOnly}
|
|
97
|
+
onClick={clickHandler}
|
|
98
|
+
onChange={changeHandler}
|
|
99
|
+
onKeyDown={enterKeyHandler}
|
|
100
|
+
autoComplete={autoComplete ? "" : "off"}
|
|
101
|
+
autoCorrect={autoComplete ? "" : "off"}
|
|
102
|
+
autoCapitalize={autoComplete ? "" : "off"}
|
|
103
|
+
spellCheck={autoComplete}
|
|
104
|
+
disabled={disabled} />
|
|
105
|
+
{icon && (onIconClick
|
|
106
|
+
? <Button
|
|
107
|
+
leftIcon={icon}
|
|
108
|
+
title={iconTitle}
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
onClick={iconClickHandler} />
|
|
111
|
+
: <i
|
|
112
|
+
className={icon}
|
|
113
|
+
aria-hidden={true} />) }
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { classList, ContainerProps } from "../util";
|
|
3
|
+
|
|
4
|
+
export interface ListProps extends ContainerProps {
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const List = (props: ListProps) => {
|
|
8
|
+
const {
|
|
9
|
+
id,
|
|
10
|
+
className,
|
|
11
|
+
ariaHidden,
|
|
12
|
+
ariaLabel,
|
|
13
|
+
role
|
|
14
|
+
} = props;
|
|
15
|
+
|
|
16
|
+
return <div
|
|
17
|
+
id={id}
|
|
18
|
+
aria-hidden={ariaHidden}
|
|
19
|
+
aria-label={ariaLabel}
|
|
20
|
+
role={role}
|
|
21
|
+
className={classList("common-list", className)}>
|
|
22
|
+
{React.Children.map(props.children, (child, index) =>
|
|
23
|
+
<div key={index} className="common-list-item">
|
|
24
|
+
{child}
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React = require("react");
|
|
2
|
+
import { classList, ContainerProps } from "../util";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
|
|
5
|
+
export interface ModalAction {
|
|
6
|
+
label: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
icon?: string;
|
|
10
|
+
xicon?: boolean;
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
url?: string;
|
|
13
|
+
|
|
14
|
+
// TODO: It would be nice to make fullscreen modals their own thing and deprecate this prop. right
|
|
15
|
+
// now it's required to render the back arrow
|
|
16
|
+
fullscreen?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ModalProps extends ContainerProps {
|
|
20
|
+
title: string;
|
|
21
|
+
ariaDescribedBy?: string;
|
|
22
|
+
actions?: ModalAction[];
|
|
23
|
+
onClose?: () => void;
|
|
24
|
+
fullscreen?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Modal = (props: ModalProps) => {
|
|
28
|
+
const {
|
|
29
|
+
children,
|
|
30
|
+
id,
|
|
31
|
+
className,
|
|
32
|
+
ariaLabel,
|
|
33
|
+
ariaHidden,
|
|
34
|
+
ariaDescribedBy,
|
|
35
|
+
role,
|
|
36
|
+
title,
|
|
37
|
+
actions,
|
|
38
|
+
onClose,
|
|
39
|
+
fullscreen
|
|
40
|
+
} = props;
|
|
41
|
+
|
|
42
|
+
const closeClickHandler = (e?: React.MouseEvent<HTMLButtonElement>) => {
|
|
43
|
+
if (onClose) onClose();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let firstFocusableElement: HTMLElement;
|
|
47
|
+
let lastFocusableElement: HTMLElement;
|
|
48
|
+
|
|
49
|
+
const handleRef = (ref: HTMLDivElement) => {
|
|
50
|
+
if (!ref) return;
|
|
51
|
+
|
|
52
|
+
const focusable = ref.querySelectorAll(`[tabindex]:not([tabindex="-1"])`);
|
|
53
|
+
|
|
54
|
+
firstFocusableElement = focusable.item(0) as HTMLElement;
|
|
55
|
+
lastFocusableElement = focusable.item(focusable.length - 1) as HTMLElement;
|
|
56
|
+
|
|
57
|
+
// TODO: Add an error here? this should never happen
|
|
58
|
+
if (!firstFocusableElement) return;
|
|
59
|
+
|
|
60
|
+
if (!ref.contains(document.activeElement)) firstFocusableElement.focus();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
|
|
64
|
+
if (e.key !== "Tab") return;
|
|
65
|
+
|
|
66
|
+
const target = e.target;
|
|
67
|
+
|
|
68
|
+
if (e.shiftKey) {
|
|
69
|
+
if (target === firstFocusableElement) {
|
|
70
|
+
lastFocusableElement.focus();
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (target === lastFocusableElement) {
|
|
76
|
+
firstFocusableElement.focus();
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const classes = classList(
|
|
83
|
+
"common-modal-container",
|
|
84
|
+
fullscreen && "fullscreen",
|
|
85
|
+
className
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return <div className={classes} ref={handleRef} onKeyDown={onKeyDown}>
|
|
89
|
+
<div id={id}
|
|
90
|
+
className="common-modal"
|
|
91
|
+
role={role || "dialog"}
|
|
92
|
+
aria-hidden={ariaHidden}
|
|
93
|
+
aria-label={ariaLabel}
|
|
94
|
+
aria-describedby={ariaDescribedBy}
|
|
95
|
+
aria-labelledby="modal-title">
|
|
96
|
+
<div className="common-modal-header">
|
|
97
|
+
{fullscreen &&
|
|
98
|
+
<div className="common-modal-back">
|
|
99
|
+
<Button
|
|
100
|
+
className="menu-button"
|
|
101
|
+
onClick={closeClickHandler}
|
|
102
|
+
title={lf("Go Back")}
|
|
103
|
+
label={lf("Go Back")}
|
|
104
|
+
leftIcon="fas fa-arrow-left"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
}
|
|
108
|
+
<div id="modal-title" className="common-modal-title">
|
|
109
|
+
{title}
|
|
110
|
+
</div>
|
|
111
|
+
{!fullscreen &&
|
|
112
|
+
<div className="common-modal-close">
|
|
113
|
+
<Button
|
|
114
|
+
className="menu-button"
|
|
115
|
+
onClick={closeClickHandler}
|
|
116
|
+
title={lf("Close")}
|
|
117
|
+
rightIcon="fas fa-times-circle"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
}
|
|
121
|
+
</div>
|
|
122
|
+
<div className="common-modal-body">
|
|
123
|
+
{children}
|
|
124
|
+
</div>
|
|
125
|
+
{actions?.length &&
|
|
126
|
+
<div className="common-modal-footer">
|
|
127
|
+
{ actions.map((action, index) =>
|
|
128
|
+
<Button
|
|
129
|
+
key={index}
|
|
130
|
+
className="primary inverted"
|
|
131
|
+
disabled={action.disabled}
|
|
132
|
+
onClick={action.onClick}
|
|
133
|
+
href={action.url}
|
|
134
|
+
label={action.label}
|
|
135
|
+
title={action.label}
|
|
136
|
+
rightIcon={(action.xicon ? "xicon " : "") + action.icon}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fireClickOnEnter } from "../util";
|
|
3
|
+
|
|
4
|
+
export interface BadgeProps {
|
|
5
|
+
onClick?: (badge: pxt.auth.Badge) => void;
|
|
6
|
+
badge: pxt.auth.Badge;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
isNew?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Badge = (props: BadgeProps) => {
|
|
12
|
+
const { badge, disabled, isNew, onClick } = props;
|
|
13
|
+
|
|
14
|
+
const onBadgeClick = onClick && (() => {
|
|
15
|
+
onClick(badge);
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const image = (disabled && badge.lockedImage) || badge.image;
|
|
19
|
+
const alt = disabled ? pxt.U.lf("Locked '{0}' badge", badge.title) : badge.title;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={`profile-badge ${onClick ? "clickable" : ""}`}
|
|
23
|
+
role={onClick ? "button" : undefined}
|
|
24
|
+
tabIndex={onClick ? 0 : undefined}
|
|
25
|
+
title={lf("{0} Badge", badge.title)}
|
|
26
|
+
onClick={onBadgeClick}
|
|
27
|
+
onKeyDown={fireClickOnEnter}>
|
|
28
|
+
{isNew && <div className="profile-badge-notification">{pxt.U.lf("New!")}</div>}
|
|
29
|
+
<img src={image} alt={alt} />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { jsxLF } from "../util";
|
|
3
|
+
import { Badge } from "./Badge";
|
|
4
|
+
|
|
5
|
+
export interface BadgeInfoProps {
|
|
6
|
+
badge: pxt.auth.Badge;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const BadgeInfo = (props: BadgeInfoProps) => {
|
|
10
|
+
const { badge } = props;
|
|
11
|
+
|
|
12
|
+
const date = new Date(badge.timestamp)
|
|
13
|
+
|
|
14
|
+
return <div className="profile-badge-info">
|
|
15
|
+
<div className="profile-badge-info-image">
|
|
16
|
+
<Badge badge={badge} disabled={!badge.timestamp} />
|
|
17
|
+
</div>
|
|
18
|
+
<div className="profile-badge-info-item">
|
|
19
|
+
<div className="profile-badge-info-header">
|
|
20
|
+
{lf("Awarded For:")}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="profile-badge-info-text">
|
|
23
|
+
{badgeDescription(badge)}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
{ badge.timestamp ?
|
|
27
|
+
<div className="profile-badge-info-item">
|
|
28
|
+
<div className="profile-badge-info-header">
|
|
29
|
+
{lf("Awarded On:")}
|
|
30
|
+
</div>
|
|
31
|
+
<div className="profile-badge-info-text">
|
|
32
|
+
{date.toLocaleDateString(pxt.U.userLanguage())}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
: undefined }
|
|
36
|
+
</div>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export const badgeDescription = (badge: pxt.auth.Badge) => {
|
|
41
|
+
switch (badge.type) {
|
|
42
|
+
case "skillmap-completion":
|
|
43
|
+
return <span>{jsxLF(
|
|
44
|
+
lf("Completing {0}"),
|
|
45
|
+
<a target="_blank" rel="noopener noreferrer" href={sourceURLToSkillmapURL(badge.sourceURL)}>{pxt.U.rlf(badge.title)}</a>
|
|
46
|
+
)}</span>
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sourceURLToSkillmapURL(sourceURL: string) {
|
|
51
|
+
if (sourceURL.indexOf("/api/md/") !== -1) {
|
|
52
|
+
// docs url: https://www.makecode.com/api/md/arcade/skillmap/forest
|
|
53
|
+
const path = sourceURL.split("/api/md/")[1];
|
|
54
|
+
// remove the target from the url
|
|
55
|
+
const docsPath = path.split("/").slice(1).join("/");
|
|
56
|
+
return pxt.webConfig?.skillmapUrl + "#docs:" + docsPath;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// github url: /user/repo#filename
|
|
60
|
+
const parts = sourceURL.split("#");
|
|
61
|
+
|
|
62
|
+
if (parts.length == 2) {
|
|
63
|
+
return pxt.webConfig.skillmapUrl + "#github:https://github.com/" + parts[0] + "/" + parts[1];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (pxt.BrowserUtils.isLocalHostDev()) {
|
|
68
|
+
// local url: skillmap/forest
|
|
69
|
+
return "http://localhost:3000#local:" + sourceURL
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return sourceURL;
|
|
73
|
+
}
|
|
74
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Badge } from "./Badge";
|
|
3
|
+
|
|
4
|
+
export interface BadgeListProps {
|
|
5
|
+
onBadgeClick: (badge: pxt.auth.Badge) => void;
|
|
6
|
+
availableBadges: pxt.auth.Badge[];
|
|
7
|
+
userState: pxt.auth.UserBadgeState;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const BadgeList = (props: BadgeListProps) => {
|
|
11
|
+
const { onBadgeClick, availableBadges, userState } = props;
|
|
12
|
+
|
|
13
|
+
const badges = availableBadges.slice();
|
|
14
|
+
|
|
15
|
+
let unlocked: pxt.Map<boolean> = {};
|
|
16
|
+
|
|
17
|
+
for (const badge of userState.badges) {
|
|
18
|
+
unlocked[badge.id] = true;
|
|
19
|
+
const existing = badges.findIndex(b => b.id === badge.id);
|
|
20
|
+
if (existing > -1) {
|
|
21
|
+
badges[existing] = {
|
|
22
|
+
...badges[existing],
|
|
23
|
+
timestamp: badges[existing].timestamp || badge.timestamp
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
badges.push(badge);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bg: JSX.Element[] = []
|
|
31
|
+
for (let i = 0; i < Math.max(badges.length + 10, 20); i++) {
|
|
32
|
+
bg.push(<div key={i} className="placeholder-badge" />)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return <div className="profile-badge-list">
|
|
36
|
+
<div className="profile-badge-header">
|
|
37
|
+
<span className="profile-badge-title">
|
|
38
|
+
{lf("Badges")}
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<span className="profile-badge-subtitle">
|
|
42
|
+
{lf("Click each badge to see details")}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="profile-badges-scroller">
|
|
46
|
+
<div className="profile-badges">
|
|
47
|
+
<div className="profile-badges-background-container">
|
|
48
|
+
<div className="profile-badges-background">
|
|
49
|
+
{ bg }
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{ badges.map(badge =>
|
|
53
|
+
<div className="profile-badge-and-title">
|
|
54
|
+
<Badge key={badge.id}
|
|
55
|
+
onClick={onBadgeClick}
|
|
56
|
+
badge={badge}
|
|
57
|
+
disabled={!unlocked[badge.id]}
|
|
58
|
+
/>
|
|
59
|
+
<div className="profile-badge-name">
|
|
60
|
+
{badge.title}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
) }
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference path="../types.d.ts" />
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { BadgeList } from "./BadgeList";
|
|
5
|
+
import { UserPane } from "./UserPane";
|
|
6
|
+
import { BadgeInfo } from "./BadgeInfo";
|
|
7
|
+
import { CheckboxStatus } from "../util";
|
|
8
|
+
|
|
9
|
+
export interface ProfileProps {
|
|
10
|
+
user: pxt.auth.State;
|
|
11
|
+
signOut: () => void;
|
|
12
|
+
deleteProfile: () => void;
|
|
13
|
+
checkedEmail: CheckboxStatus;
|
|
14
|
+
onClickedEmail: (isChecked: boolean) => void;
|
|
15
|
+
notification?: pxt.ProfileNotification;
|
|
16
|
+
showModalAsync(options: DialogOptions): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Profile = (props: ProfileProps) => {
|
|
20
|
+
const { user, signOut, deleteProfile, onClickedEmail, notification, checkedEmail, showModalAsync } = props;
|
|
21
|
+
const userProfile = user?.profile || { idp: {} };
|
|
22
|
+
const userBadges = user?.preferences?.badges || { badges: [] };
|
|
23
|
+
|
|
24
|
+
const onBadgeClick = (badge: pxt.auth.Badge) => {
|
|
25
|
+
showModalAsync({
|
|
26
|
+
header: lf("{0} Badge", badge.title),
|
|
27
|
+
size: "tiny",
|
|
28
|
+
hasCloseIcon: true,
|
|
29
|
+
jsx: <BadgeInfo badge={badge} />
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <div className="user-profile">
|
|
34
|
+
<UserPane profile={userProfile} onSignOutClick={signOut} onDeleteProfileClick={deleteProfile} notification={notification}
|
|
35
|
+
emailChecked={checkedEmail} onEmailCheckClick={onClickedEmail}/>
|
|
36
|
+
<BadgeList
|
|
37
|
+
availableBadges={pxt.appTarget.defaultBadges || []}
|
|
38
|
+
userState={userBadges}
|
|
39
|
+
onBadgeClick={onBadgeClick}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface UserNotificationProps {
|
|
4
|
+
notification: pxt.ProfileNotification;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const UserNotification = (props: UserNotificationProps) => {
|
|
8
|
+
const { message, icon, actionText, link, xicon, title } = props.notification;
|
|
9
|
+
|
|
10
|
+
const onActionClick = () => {
|
|
11
|
+
window.open(link, "_blank");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="profile-notification">
|
|
17
|
+
<div className="profile-notification-icon">
|
|
18
|
+
<i className={`${xicon ? "xicon" : "ui large circular icon "} ${icon}`} />
|
|
19
|
+
</div>
|
|
20
|
+
<div className="profile-notification-title">
|
|
21
|
+
{title}
|
|
22
|
+
</div>
|
|
23
|
+
<div className="profile-notification-message">
|
|
24
|
+
{message}
|
|
25
|
+
</div>
|
|
26
|
+
<button className="ui icon button profile-notification-button" onClick={onActionClick} role="link" >
|
|
27
|
+
<i className="icon external alternate"></i>
|
|
28
|
+
{actionText}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { fireClickOnEnter, CheckboxStatus } from "../util";
|
|
3
|
+
import { UserNotification } from "./UserNotification";
|
|
4
|
+
import { Checkbox } from "../controls/Checkbox";
|
|
5
|
+
|
|
6
|
+
export interface UserPaneProps {
|
|
7
|
+
profile: pxt.auth.UserProfile;
|
|
8
|
+
notification?: pxt.ProfileNotification;
|
|
9
|
+
emailChecked: CheckboxStatus;
|
|
10
|
+
|
|
11
|
+
onSignOutClick: () => void;
|
|
12
|
+
onDeleteProfileClick: () => void;
|
|
13
|
+
onEmailCheckClick: (isChecked: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const UserPane = (props: UserPaneProps) => {
|
|
17
|
+
const { profile, onSignOutClick, onDeleteProfileClick, onEmailCheckClick, notification, emailChecked } = props;
|
|
18
|
+
|
|
19
|
+
const { username, displayName, picture } = profile.idp;
|
|
20
|
+
|
|
21
|
+
const emailLabel = <>
|
|
22
|
+
{emailChecked === CheckboxStatus.Waiting ? <div className="common-spinner" /> : undefined}
|
|
23
|
+
{lf("I would like to receive the MakeCode newsletter. ")}
|
|
24
|
+
<a href="https://makecode.com/privacy" target="_blank" rel="noopener noreferrer">{lf("View Privacy Statement")}</a>
|
|
25
|
+
</>
|
|
26
|
+
|
|
27
|
+
return <div className="profile-user-pane">
|
|
28
|
+
<div className="profile-portrait">
|
|
29
|
+
{ picture?.dataUrl ?
|
|
30
|
+
<img src={picture?.dataUrl} alt={pxt.U.lf("Profile Picture")} />
|
|
31
|
+
: <div className="profile-initials-portrait">
|
|
32
|
+
{pxt.auth.userInitials(profile)}
|
|
33
|
+
</div>
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
36
|
+
<div className="profile-user-details">
|
|
37
|
+
<div className="profile-display-name">
|
|
38
|
+
{displayName}
|
|
39
|
+
</div>
|
|
40
|
+
{ username &&
|
|
41
|
+
<div className="profile-username">
|
|
42
|
+
{username}
|
|
43
|
+
</div>
|
|
44
|
+
}
|
|
45
|
+
</div>
|
|
46
|
+
{ notification && <UserNotification notification={notification}/> }
|
|
47
|
+
<div className="profile-spacer"></div>
|
|
48
|
+
<div className="profile-email">
|
|
49
|
+
<Checkbox id="profile-email-checkbox"
|
|
50
|
+
className={emailChecked === CheckboxStatus.Waiting ? "loading" : ""}
|
|
51
|
+
isChecked={emailChecked === CheckboxStatus.Selected}
|
|
52
|
+
onChange={onEmailCheckClick}
|
|
53
|
+
label={emailLabel}/>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="profile-actions">
|
|
56
|
+
<a role="button"
|
|
57
|
+
tabIndex={0}
|
|
58
|
+
onKeyPress={fireClickOnEnter}
|
|
59
|
+
onClick={onDeleteProfileClick}>
|
|
60
|
+
{lf("Delete Profile")}
|
|
61
|
+
</a>
|
|
62
|
+
<button onClick={onSignOutClick} className="ui icon button sign-out">
|
|
63
|
+
<i className="icon sign-out"></i>
|
|
64
|
+
{lf("Sign Out")}
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/// <reference path="../../built/pxtlib.d.ts" />
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
interface DialogOptions {
|
|
5
|
+
type?: string;
|
|
6
|
+
hideCancel?: boolean;
|
|
7
|
+
disagreeLbl?: string;
|
|
8
|
+
disagreeClass?: string;
|
|
9
|
+
disagreeIcon?: string;
|
|
10
|
+
logos?: string[];
|
|
11
|
+
className?: string;
|
|
12
|
+
header: string;
|
|
13
|
+
headerIcon?: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
jsx?: JSX.Element;
|
|
16
|
+
jsxd?: () => JSX.Element; // dynamic-er version of jsx
|
|
17
|
+
copyable?: string;
|
|
18
|
+
size?: "" | "small" | "fullscreen" | "large" | "mini" | "tiny"; // defaults to "small"
|
|
19
|
+
onLoaded?: (_: HTMLElement) => void;
|
|
20
|
+
// buttons?: sui.ModalButton[];
|
|
21
|
+
timeout?: number;
|
|
22
|
+
modalContext?: string;
|
|
23
|
+
hasCloseIcon?: boolean;
|
|
24
|
+
helpUrl?: string;
|
|
25
|
+
bigHelpButton?: boolean;
|
|
26
|
+
confirmationText?: string; // Display a text input the user must type to confirm.
|
|
27
|
+
confirmationCheckbox?: string; // Display a checkbox the user must check to confirm.
|
|
28
|
+
confirmationGranted?: boolean;
|
|
29
|
+
}
|