diff --git a/.gitignore b/.gitignore index 0a75f3116eab..449b9390936d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ webapp/public/authcode.html webapp/public/multiplayer.html webapp/public/kiosk.html webapp/public/teachertool.html +webapp/public/tutorialtool.html localtypings/blockly.d.ts node_modules *.sw? diff --git a/cli/webapps-config.json b/cli/webapps-config.json index 048acb7f28e3..90d261777bfa 100644 --- a/cli/webapps-config.json +++ b/cli/webapps-config.json @@ -12,6 +12,12 @@ "localServeWebConfigUrl": false, "localServeEndpoint": "eval" }, + { + "name": "tutorialtool", + "buildCss": false, + "localServeWebConfigUrl": false, + "localServeEndpoint": "tt" + }, { "name": "skillmap", "buildCss": true, diff --git a/gulpfile.js b/gulpfile.js index 72b6902f2fd7..1abd2b1315cb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -590,6 +590,12 @@ const kiosk = createWebappTasks("kiosk"); const teacherTool = createWebappTasks("teachertool"); +/******************************************************** + Tutorial Tool +*********************************************************/ + +const tutorialTool = createWebappTasks("tutorialtool"); + /******************************************************** Webapp build wrappers *********************************************************/ @@ -604,7 +610,7 @@ const maybeUpdateWebappStrings = () => { const maybeBuildWebapps = () => { if (!shouldBuildWebapps()) return noop; - return gulp.parallel(skillmap, authcode, multiplayer, kiosk, teacherTool); + return gulp.parallel(skillmap, authcode, multiplayer, kiosk, teacherTool, tutorialTool); } /******************************************************** diff --git a/pxtlib/shell.ts b/pxtlib/shell.ts index ad9c09f86c27..4b48475255b7 100644 --- a/pxtlib/shell.ts +++ b/pxtlib/shell.ts @@ -95,5 +95,4 @@ namespace pxt.shell { export function setToolboxAnimation(): void { pxt.storage.setLocal("toolboxanimation", "1"); } - } \ No newline at end of file diff --git a/pxtservices/backendRequests.ts b/pxtservices/backendRequests.ts new file mode 100644 index 000000000000..325107933cea --- /dev/null +++ b/pxtservices/backendRequests.ts @@ -0,0 +1,64 @@ +import { ErrorCode } from "./constants"; +import { logError } from "./loggingService"; + +export async function fetchJsonDocAsync(url: string): Promise { + try { + const response = await fetch(url, { + cache: "no-cache", + }); + if (!response.ok) { + throw new Error("Unable to fetch the json file"); + } else { + const json = await response.json(); + return json; + } + } catch (e) { + logError(ErrorCode.fetchJsonDocAsync, e); + } + + return undefined; +} + +export async function getProjectTextAsync(projectId: string): Promise { + try { + const projectTextUrl = `${pxt.Cloud.apiRoot}/${projectId}/text`; + const response = await fetch(projectTextUrl); + if (!response.ok) { + throw new Error("Unable to fetch the project details"); + } else { + const projectText = await response.json(); + return projectText; + } + } catch (e) { + logError(ErrorCode.getProjectTextAsync, e); + } + + return undefined; +} + +export async function getProjectMetaAsync(projectId: string): Promise { + try { + const projectMetaUrl = `${pxt.Cloud.apiRoot}/${projectId}`; + const response = await fetch(projectMetaUrl); + if (!response.ok) { + throw new Error("Unable to fetch the project meta information"); + } else { + const projectMeta = await response.json(); + return projectMeta; + } + } catch (e) { + logError(ErrorCode.getProjectMetaAsync, e); + } + + return undefined; +} + +export async function downloadTargetConfigAsync(): Promise { + try { + return await pxt.targetConfigAsync(); + } catch (e) { + logError(ErrorCode.downloadTargetConfigAsync, e); + } + + return undefined; +} diff --git a/pxtservices/constants.ts b/pxtservices/constants.ts new file mode 100644 index 000000000000..5ee87bc658d7 --- /dev/null +++ b/pxtservices/constants.ts @@ -0,0 +1,6 @@ +export enum ErrorCode { + downloadTargetConfigAsync = "downloadTargetConfigAsync", + fetchJsonDocAsync = "fetchJsonDocAsync", + getProjectTextAsync = "getProjectTextAsync", + getProjectMetaAsync = "getProjectMetaAsync", +} \ No newline at end of file diff --git a/pxtservices/loggingService.ts b/pxtservices/loggingService.ts new file mode 100644 index 000000000000..0ff438ce4aeb --- /dev/null +++ b/pxtservices/loggingService.ts @@ -0,0 +1,49 @@ +let tickEvent = "pxt.error"; + +const timestamp = () => { + const time = new Date(); + const hours = padTime(time.getHours()); + const minutes = padTime(time.getMinutes()); + const seconds = padTime(time.getSeconds()); + + return `[${hours}:${minutes}:${seconds}]`; +}; + +const padTime = (time: number) => ("0" + time).slice(-2); + +export const logError = (errorCode: string, message?: any, data: pxt.Map = {}) => { + let dataObj = { ...data }; + if (message) { + if (typeof message === "object") { + dataObj = { ...dataObj, ...message }; + // Look for non-enumerable properties found on Error objects + ["message", "stack", "name"].forEach(key => { + if (message[key]) { + dataObj[key] = message[key]; + } + }); + } else { + dataObj.message = message; + } + } + pxt.tickEvent(tickEvent, { + ...dataObj, + errorCode, + }); + console.error(timestamp(), errorCode, dataObj); +}; + +export const logInfo = (message: any) => { + console.log(timestamp(), message); +}; + +export const logDebug = (message: any, data?: any) => { + if (pxt.BrowserUtils.isLocalHost() || pxt.options.debug) { + console.log(timestamp(), message, data); + } +}; + + +export const setTickEvent = (event: string) => { + tickEvent = event; +} \ No newline at end of file diff --git a/react-common/components/profile/SignInButton.tsx b/react-common/components/profile/SignInButton.tsx new file mode 100644 index 000000000000..655387fff869 --- /dev/null +++ b/react-common/components/profile/SignInButton.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { Button } from "../controls/Button"; +import { classList } from "../util"; + +interface IProps { + onSignInClick: () => void; + className?: string; +} + +export const SignInButton: React.FC = ({ onSignInClick, className }) => { + return ( +