diff --git a/src/components/youtube-video-button/youtube-video-button.jsx b/src/components/youtube-video-button/youtube-video-button.jsx
new file mode 100644
index 000000000..5868b77b6
--- /dev/null
+++ b/src/components/youtube-video-button/youtube-video-button.jsx
@@ -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}) => (
+
onSelectedVideo(videoData.videoId)}
+ >
+
+
+
{videoData.length}
+
+
+
{videoData.title}
+
+
{videoData.channel}
+
+ {`${parseViewCount(videoData.views)} ยท ${videoData.uploadTime}`}
+
+
+ {videoData.hasCC &&
}
+
+
+);
+
+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
+};
diff --git a/src/components/youtube-video-button/youtube-video-button.scss b/src/components/youtube-video-button/youtube-video-button.scss
new file mode 100644
index 000000000..9ff0a6502
--- /dev/null
+++ b/src/components/youtube-video-button/youtube-video-button.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/components/youtube-video-modal/youtube-video-modal.jsx b/src/components/youtube-video-modal/youtube-video-modal.jsx
new file mode 100644
index 000000000..60d503628
--- /dev/null
+++ b/src/components/youtube-video-modal/youtube-video-modal.jsx
@@ -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 (
+
+
+
+
+
+
+ );
+};
+
+YoutubeVideoModal.defaultProps = {
+ className: 'mint-green'
+};
+
+YoutubeVideoModal.propTypes = {
+ videoId: PropTypes.string,
+ onClose: PropTypes.func,
+ className: PropTypes.string
+};
diff --git a/src/components/youtube-video-modal/youtube-video-modal.scss b/src/components/youtube-video-modal/youtube-video-modal.scss
new file mode 100644
index 000000000..bb7baef44
--- /dev/null
+++ b/src/components/youtube-video-modal/youtube-video-modal.scss
@@ -0,0 +1,58 @@
+@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;
+ }
+}
+
+.youtube-video-modal-container:focus {
+ outline: none;
+ border: none;
+}
diff --git a/src/views/ideas/ideas.jsx b/src/views/ideas/ideas.jsx
index 41214dfb4..ef381b9a9 100644
--- a/src/views/ideas/ideas.jsx
+++ b/src/views/ideas/ideas.jsx
@@ -1,5 +1,8 @@
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
+const {useState, useCallback, useEffect} = require('react');
+const PropTypes = require('prop-types');
+const api = require('../../lib/api');
const Button = require('../../components/forms/button.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
@@ -9,6 +12,13 @@ const Page = require('../../components/page/www/page.jsx');
const render = require('../../lib/render.jsx');
const {useIntl} = require('react-intl');
+const {
+ YoutubeVideoButton
+} = require('../../components/youtube-video-button/youtube-video-button.jsx');
+const {
+ YoutubeVideoModal
+} = require('../../components/youtube-video-modal/youtube-video-modal.jsx');
+const Spinner = require('../../components/spinner/spinner.jsx');
require('./ideas.scss');
@@ -85,8 +95,72 @@ const physicalIdeasData = [
}
];
+const playlists = {
+ 'sprites-and-vectors': 'ideas.spritesAndVector',
+ 'tips-and-tricks': 'ideas.tipsAndTricks',
+ 'advanced-topics': 'ideas.advancedTopics'
+};
+
+const PlaylistItem = ({playlistKey, onSelectedVideo}) => {
+ const [loading, setLoading] = useState(true);
+ const [playlistVideos, setPlaylistVideos] = useState([]);
+
+ useEffect(() => {
+ api({
+ host: process.env.ROOT_URL,
+ method: 'GET',
+ uri: `/ideas/videos/${playlistKey}`,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }, (_err, body, res) => {
+ setLoading(false);
+ if (res.statusCode === 200) {
+ setPlaylistVideos(body);
+ }
+ });
+ }, []);
+
+ return (
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+ {playlistVideos
+ .sort(
+ (firstVideo, secondVideo) =>
+ new Date(firstVideo.publishedAt).getTime() <
+ new Date(secondVideo.publishedAt).getTime()
+ )
+ .map(video => (
+
+ ))}
+
+ )}
+
+ );
+};
+
const Ideas = () => {
const intl = useIntl();
+ const [youtubeVideoId, setYoutubeVideoId] = useState('');
+
+ const onCloseVideoModal = useCallback(() => setYoutubeVideoId(''), [setYoutubeVideoId]);
+ const onSelectedVideo = useCallback(
+ videoId => setYoutubeVideoId(videoId),
+ [setYoutubeVideoId]
+ );
return (
@@ -117,10 +191,7 @@ const Ideas = () => {
-
+
{tipsSectionData.map((tipData, index) => (
{
))}
-
+
+
+
+
+
+
+
+
+
+
+ {Object.keys(playlists).map(playlistKey => (
+
+ ))}
+
+
@@ -200,7 +308,9 @@ const Ideas = () => {
}
/>
@@ -258,6 +368,11 @@ const Ideas = () => {
);
};
+PlaylistItem.propTypes = {
+ playlistKey: PropTypes.string,
+ onSelectedVideo: PropTypes.func
+};
+
render(
diff --git a/src/views/ideas/ideas.scss b/src/views/ideas/ideas.scss
index 8354e8354..7b5b788db 100644
--- a/src/views/ideas/ideas.scss
+++ b/src/views/ideas/ideas.scss
@@ -24,8 +24,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;
@@ -40,6 +42,83 @@ $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;
+ }
+ }
+}
+
+.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;
+ }
+}
+
.physical-ideas {
.inner {
display: flex;
@@ -124,10 +203,14 @@ $base-bg: $ui-white;
}
}
-.section-header {
- font-size: 2rem;
- font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
- font-weight: 700;
+.tips, .physical-ideas, .playlist {
+ .section-header {
+ display: flex;
+ justify-content: start;
+ font-size: 2rem;
+ font-weight: 700;
+ line-height: 2.5rem;
+ }
}
.tips-button {
@@ -136,7 +219,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;
diff --git a/src/views/ideas/l10n.json b/src/views/ideas/l10n.json
index fb5965869..87496b7ad 100644
--- a/src/views/ideas/l10n.json
+++ b/src/views/ideas/l10n.json
@@ -31,6 +31,11 @@
"ideas.tryTheTutorial": "Try the tutorial",
"ideas.codingCards": "Coding Cards",
"ideas.educatorGuide": "Educator Guide",
+ "ideas.scratchYouTubeChannel": "ScratchTeam channel",
+ "ideas.scratchYouTubeChannelDescription": "This is the official Youtube Channel 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.",
diff --git a/static/images/ideas/video-cc-label.svg b/static/images/ideas/video-cc-label.svg
new file mode 100644
index 000000000..c8fb418e2
--- /dev/null
+++ b/static/images/ideas/video-cc-label.svg
@@ -0,0 +1,4 @@
+
diff --git a/static/images/ideas/youtube-icon.svg b/static/images/ideas/youtube-icon.svg
new file mode 100644
index 000000000..6ef5af067
--- /dev/null
+++ b/static/images/ideas/youtube-icon.svg
@@ -0,0 +1,12 @@
+
diff --git a/webpack.config.js b/webpack.config.js
index e62fa08c5..590df8b9f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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.ly'}"`,
'process.env.RECAPTCHA_SITE_KEY': `"${
process.env.RECAPTCHA_SITE_KEY || '6Lf6kK4UAAAAABKTyvdSqgcSVASEnMrCquiAkjVW'}"`,
'process.env.ASSET_HOST': `"${process.env.ASSET_HOST || 'https://assets.scratch.mit.edu'}"`,