mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-12-11 16:21:04 -05:00
Merge pull request #8984 from MiroslavDionisiev/UEPR-116
feat: [UEPR-116] add youtube video playlists and open videos in modal
This commit is contained in:
commit
9210e3700d
12 changed files with 454 additions and 13 deletions
|
@ -0,0 +1,61 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import api from '../../lib/api';
|
||||
import {YoutubeVideoButton} from '../youtube-video-button/youtube-video-button.jsx';
|
||||
import Spinner from '../spinner/spinner.jsx';
|
||||
|
||||
import './youtube-playlist-item.scss';
|
||||
|
||||
export const YoutubePlaylistItem = ({playlistRequestUri, playlistTitleId, onSelectedVideo}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [playlistVideos, setPlaylistVideos] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
api({
|
||||
host: process.env.ROOT_URL,
|
||||
method: 'GET',
|
||||
uri: playlistRequestUri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, (_err, body, res) => {
|
||||
setLoading(false);
|
||||
if (res.statusCode === 200) {
|
||||
setPlaylistVideos(body);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="playlist">
|
||||
<div className="playlist-title">
|
||||
<FormattedMessage id={playlistTitleId} />
|
||||
</div>
|
||||
{loading ? (
|
||||
<Spinner
|
||||
className="spinner"
|
||||
color="transparent-gray"
|
||||
/>
|
||||
) : (
|
||||
<section className="playlist-videos">
|
||||
{playlistVideos
|
||||
.map(video => (
|
||||
<YoutubeVideoButton
|
||||
key={video.videoId}
|
||||
onSelectedVideo={onSelectedVideo}
|
||||
{...video}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
YoutubePlaylistItem.propTypes = {
|
||||
playlistRequestUri: PropTypes.string,
|
||||
playlistTitleId: PropTypes.string,
|
||||
onSelectedVideo: PropTypes.func
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
.playlist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.playlist-title {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.playlist-videos {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(13.125rem, auto));
|
||||
justify-content: space-around;
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
45
src/components/youtube-video-button/youtube-video-button.jsx
Normal file
45
src/components/youtube-video-button/youtube-video-button.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './youtube-video-button.scss';
|
||||
|
||||
const parseViewCount = viewCount =>
|
||||
parseInt(viewCount, 10).toLocaleString('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short'
|
||||
});
|
||||
|
||||
export const YoutubeVideoButton = ({onSelectedVideo, ...videoData}) => (
|
||||
<div
|
||||
className="youtbe-video-button"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onSelectedVideo(videoData.videoId)}
|
||||
>
|
||||
<div className="thumbnail">
|
||||
<img src={videoData.thumbnail} />
|
||||
<div className="duration">{videoData.length}</div>
|
||||
</div>
|
||||
<div className="video-info">
|
||||
<div className="video-title">{videoData.title}</div>
|
||||
<div className="video-metadata">
|
||||
<div className="channel-name">{videoData.channel}</div>
|
||||
<div className="video-statistics">
|
||||
{`${parseViewCount(videoData.views)} · ${videoData.uploadTime}`}
|
||||
</div>
|
||||
</div>
|
||||
{videoData.hasCC && <img src="/images/ideas/video-cc-label.svg" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
YoutubeVideoButton.propTypes = {
|
||||
videoId: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
channel: PropTypes.string,
|
||||
uploadTime: PropTypes.string,
|
||||
length: PropTypes.string,
|
||||
views: PropTypes.string,
|
||||
hasCC: PropTypes.bool,
|
||||
onSelectedVideo: PropTypes.func
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
@import "../../colors";
|
||||
|
||||
.youtbe-video-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
width: 13.125rem;
|
||||
|
||||
.thumbnail {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.duration {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom : 0.5rem;
|
||||
right: 0;
|
||||
|
||||
margin: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
background-color: $overlay-gray;
|
||||
color: $type-white;
|
||||
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-right: 1.5rem;
|
||||
text-align: start;
|
||||
|
||||
.video-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-height: 1.2rem;
|
||||
max-height: 2.4rem;
|
||||
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.video-metadata {
|
||||
margin: 0.5rem 4rem 0.25rem 0;
|
||||
|
||||
color: $ui-dark-gray;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
47
src/components/youtube-video-modal/youtube-video-modal.jsx
Normal file
47
src/components/youtube-video-modal/youtube-video-modal.jsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '../forms/button.jsx';
|
||||
const classNames = require('classnames');
|
||||
|
||||
import './youtube-video-modal.scss';
|
||||
|
||||
export const YoutubeVideoModal = ({videoId, onClose = () => {}, className}) => {
|
||||
if (!videoId) return null;
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={!!videoId}
|
||||
onRequestClose={onClose}
|
||||
className="youtube-video-modal-container"
|
||||
overlayClassName="youtube-video-modal-overlay"
|
||||
>
|
||||
<div
|
||||
className={classNames('cards-modal-header', className)}
|
||||
>
|
||||
<Button
|
||||
className="close-button"
|
||||
isCloseType
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
<iframe
|
||||
className="youtube-player"
|
||||
type="text/html"
|
||||
width="640"
|
||||
height="360"
|
||||
src={`https://www.youtube.com/embed/${videoId}?rel=0&cc_load_policy=1&autoplay=1`}
|
||||
allow="autoplay"
|
||||
/>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
YoutubeVideoModal.defaultProps = {
|
||||
className: 'mint-green'
|
||||
};
|
||||
|
||||
YoutubeVideoModal.propTypes = {
|
||||
videoId: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
className: PropTypes.string
|
||||
};
|
60
src/components/youtube-video-modal/youtube-video-modal.scss
Normal file
60
src/components/youtube-video-modal/youtube-video-modal.scss
Normal file
|
@ -0,0 +1,60 @@
|
|||
@import "../../colors";
|
||||
|
||||
.youtube-video-modal-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 510;
|
||||
|
||||
background-color: $box-shadow-light-gray;
|
||||
border-color: unset;
|
||||
}
|
||||
|
||||
.youtube-video-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 640px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cards-modal-header {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
|
||||
padding: 10px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
.close-button {
|
||||
position: unset;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.mint-green {
|
||||
background-color: $ui-mint-green;
|
||||
}
|
||||
}
|
||||
|
||||
.youtube-player {
|
||||
border: 0;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.youtube-video-modal-container:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
const React = require('react');
|
||||
const {useState, useCallback} = require('react');
|
||||
|
||||
const Button = require('../../components/forms/button.jsx');
|
||||
const FlexRow = require('../../components/flex-row/flex-row.jsx');
|
||||
|
@ -8,6 +9,10 @@ const Page = require('../../components/page/www/page.jsx');
|
|||
const render = require('../../lib/render.jsx');
|
||||
|
||||
const {useIntl} = require('react-intl');
|
||||
const {
|
||||
YoutubeVideoModal
|
||||
} = require('../../components/youtube-video-modal/youtube-video-modal.jsx');
|
||||
const {YoutubePlaylistItem} = require('../../components/youtube-playlist-item/youtube-playlist-item.jsx');
|
||||
|
||||
require('./ideas.scss');
|
||||
|
||||
|
@ -84,8 +89,21 @@ const physicalIdeasData = [
|
|||
}
|
||||
];
|
||||
|
||||
const playlists = {
|
||||
'sprites-and-vectors': 'ideas.spritesAndVector',
|
||||
'tips-and-tricks': 'ideas.tipsAndTricks',
|
||||
'advanced-topics': 'ideas.advancedTopics'
|
||||
};
|
||||
|
||||
const Ideas = () => {
|
||||
const intl = useIntl();
|
||||
const [youtubeVideoId, setYoutubeVideoId] = useState('');
|
||||
|
||||
const onCloseVideoModal = useCallback(() => setYoutubeVideoId(''), [setYoutubeVideoId]);
|
||||
const onSelectedVideo = useCallback(
|
||||
videoId => setYoutubeVideoId(videoId),
|
||||
[setYoutubeVideoId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -111,10 +129,7 @@ const Ideas = () => {
|
|||
<div className="section-header">
|
||||
<FormattedMessage id="ideas.startHereText" />
|
||||
</div>
|
||||
<FlexRow
|
||||
as="section"
|
||||
className="tips-section"
|
||||
>
|
||||
<section className="tips-section">
|
||||
{tipsSectionData.map((tipData, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
@ -148,7 +163,45 @@ const Ideas = () => {
|
|||
</a>
|
||||
</div>
|
||||
))}
|
||||
</FlexRow>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div className="youtube-videos">
|
||||
<div className="inner">
|
||||
<div className="section-header">
|
||||
<img src="/images/ideas/youtube-icon.svg" />
|
||||
<div>
|
||||
<div className="section-title">
|
||||
<FormattedMessage id="ideas.scratchYouTubeChannel" />
|
||||
</div>
|
||||
<div className="section-description">
|
||||
<FormattedMessage
|
||||
id="ideas.scratchYouTubeChannelDescription"
|
||||
values={{
|
||||
a: chunks => (
|
||||
<a href="https://www.youtube.com/@ScratchTeam">
|
||||
{chunks}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="playlists">
|
||||
{Object.keys(playlists).map(playlistKey => (
|
||||
<YoutubePlaylistItem
|
||||
key={playlistKey}
|
||||
playlistRequestUri={`/ideas/videos/${playlistKey}`}
|
||||
playlistTitleId={playlists[playlistKey]}
|
||||
onSelectedVideo={onSelectedVideo}
|
||||
/>
|
||||
))}
|
||||
<YoutubeVideoModal
|
||||
videoId={youtubeVideoId}
|
||||
onClose={onCloseVideoModal}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div className="physical-ideas">
|
||||
|
@ -194,7 +247,9 @@ const Ideas = () => {
|
|||
}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id={physicalIdea.physicalIdeasDescription.buttonTextId}
|
||||
id={
|
||||
physicalIdea.physicalIdeasDescription.buttonTextId
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</a>
|
||||
|
|
|
@ -52,8 +52,10 @@ $base-bg: $ui-white;
|
|||
gap: 0.75rem;
|
||||
|
||||
.tips-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax($cols3, auto));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.tip {
|
||||
display: flex;
|
||||
|
@ -68,6 +70,56 @@ $base-bg: $ui-white;
|
|||
}
|
||||
}
|
||||
|
||||
.youtube-videos {
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2.5rem;
|
||||
padding: 4rem 0;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-width: 25rem;
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 2.5rem;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5rem;
|
||||
text-align: start;
|
||||
|
||||
a {
|
||||
font-weight: 400;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlists {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.physical-ideas {
|
||||
.inner {
|
||||
display: flex;
|
||||
|
@ -152,10 +204,14 @@ $base-bg: $ui-white;
|
|||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 2rem;
|
||||
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
||||
font-weight: 700;
|
||||
.tips, .physical-ideas {
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tips-button {
|
||||
|
@ -164,7 +220,6 @@ $base-bg: $ui-white;
|
|||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
||||
font-weight: 700;
|
||||
line-height: 1.25rem;
|
||||
|
||||
|
|
|
@ -32,6 +32,11 @@
|
|||
"ideas.tryTheTutorial": "Try the tutorial",
|
||||
"ideas.codingCards": "Coding Cards",
|
||||
"ideas.educatorGuide": "Educator Guide",
|
||||
"ideas.scratchYouTubeChannel": "ScratchTeam channel",
|
||||
"ideas.scratchYouTubeChannelDescription": "This is the official <a>Youtube Channel</a> of Scratch. We share resources, tutorials, and stories about Scratch.",
|
||||
"ideas.spritesAndVector": "Sprites & Vector Drawing",
|
||||
"ideas.tipsAndTricks": "Tips & Tricks",
|
||||
"ideas.advancedTopics": "Advanced Topics",
|
||||
"ideas.physicalPlayIdeas": "Physical Play Ideas",
|
||||
"ideas.microBitHeader": "Have a micro:bit?",
|
||||
"ideas.microBitBody": "Connect your Scratch project to the real world.",
|
||||
|
|
4
static/images/ideas/video-cc-label.svg
Normal file
4
static/images/ideas/video-cc-label.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="0.0300293" width="15" height="16" fill="#606060"/>
|
||||
<path d="M15 7.03003H13.5V6.53003H11.5V9.53003H13.5V9.03003H15V10.03C15 10.2952 14.8946 10.5496 14.7071 10.7371C14.5196 10.9247 14.2652 11.03 14 11.03H11C10.7348 11.03 10.4804 10.9247 10.2929 10.7371C10.1054 10.5496 10 10.2952 10 10.03V6.03003C10 5.76481 10.1054 5.51046 10.2929 5.32292C10.4804 5.13539 10.7348 5.03003 11 5.03003H14C14.2652 5.03003 14.5196 5.13539 14.7071 5.32292C14.8946 5.51046 15 5.76481 15 6.03003M8 7.03003H6.5V6.53003H4.5V9.53003H6.5V9.03003H8V10.03C8 10.2952 7.89464 10.5496 7.70711 10.7371C7.51957 10.9247 7.26522 11.03 7 11.03H4C3.73478 11.03 3.48043 10.9247 3.29289 10.7371C3.10536 10.5496 3 10.2952 3 10.03V6.03003C3 5.76481 3.10536 5.51046 3.29289 5.32292C3.48043 5.13539 3.73478 5.03003 4 5.03003H7C7.26522 5.03003 7.51957 5.13539 7.70711 5.32292C7.89464 5.51046 8 5.76481 8 6.03003M16 0.0300293H2C0.89 0.0300293 0 0.920029 0 2.03003V14.03C0 14.5605 0.210714 15.0692 0.585786 15.4442C0.960859 15.8193 1.46957 16.03 2 16.03H16C16.5304 16.03 17.0391 15.8193 17.4142 15.4442C17.7893 15.0692 18 14.5605 18 14.03V2.03003C18 1.4996 17.7893 0.990888 17.4142 0.615816C17.0391 0.240743 16.5304 0.0300293 16 0.0300293Z" fill="#F4F4F4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
12
static/images/ideas/youtube-icon.svg
Normal file
12
static/images/ideas/youtube-icon.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<svg width="140" height="110" viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2_749)">
|
||||
<path opacity="0.15" fill-rule="evenodd" clip-rule="evenodd" d="M55 102.36C23.62 102.06 -6.70002 64.7 2.64998 39.56C12.01 14.43 29.31 -0.300021 60.69 -2.09711e-05C92.07 0.299979 118.24 30.85 123.1 63.38C127.97 95.91 86.38 102.65 55 102.35V102.36ZM22.16 108.18C17.61 111.53 10.57 104.29 6.26998 99.23C1.96998 94.17 3.61998 88.8 7.47998 88.84C10.12 88.87 12.94 91.03 15.54 93.07L15.79 93.27C16.89 94.13 17.95 94.96 18.93 95.57C22.3 97.67 26.71 104.83 22.16 108.18Z" fill="#628EC9"/>
|
||||
<path d="M139.01 57.4C138.99 65.5 133.84 71.28 126.28 73.12C118.11 75.11 111.03 76.27 93.01 78.16C74.39 80.12 67.76 80.36 59.32 80.15C51.29 79.95 45.15 75.44 43.6 67.42C43.28 65.76 41.85 60.53 40.91 51.64C40.24 45.22 40.47 38.9 40.44 37.34C40.3 29.32 45.38 23.67 53.17 21.62C61.33 19.47 67.42 18.32 86.51 16.31C105.55 14.31 111.72 14.38 120.13 14.58C128.33 14.78 134.34 19.13 135.85 27.31C136.14 28.9 137.59 34.09 138.44 42.16C139.31 50.45 139.01 55.76 139.01 57.39V57.4Z" fill="#FF0000"/>
|
||||
<path d="M84.33 59.9L104.45 45.28L81.73 35.16L84.33 59.9Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2_749">
|
||||
<rect width="138.14" height="109.03" fill="white" transform="translate(0.929993)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -271,6 +271,7 @@ module.exports = {
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': `"${process.env.NODE_ENV || 'development'}"`,
|
||||
'process.env.API_HOST': `"${process.env.API_HOST || 'https://api.scratch.mit.edu'}"`,
|
||||
'process.env.ROOT_URL': `"${process.env.ROOT_URL || 'https://scratch.mit.edu'}"`,
|
||||
'process.env.RECAPTCHA_SITE_KEY': `"${
|
||||
process.env.RECAPTCHA_SITE_KEY || '6Lf6kK4UAAAAABKTyvdSqgcSVASEnMrCquiAkjVW'}"`,
|
||||
'process.env.ASSET_HOST': `"${process.env.ASSET_HOST || 'https://assets.scratch.mit.edu'}"`,
|
||||
|
|
Loading…
Reference in a new issue