Merge remote-tracking branch 'origin/integration-branch-ux-12.2024' into UEPR-88

This commit is contained in:
MiroslavDionisiev 2024-12-04 15:39:01 +02:00
commit 44295ea514
14 changed files with 527 additions and 94 deletions

View file

@ -33,6 +33,7 @@ $ui-cyan-blue: hsla(194, 73%, 36%, 1); //#19809F
/* Using www naming convention for now, should be consistent with gui */
$ui-aqua: hsla(144, 45%, 36%, 1);
$ui-aqua-dark: darken($ui-aqua, 10%);
$ui-purple-light: hsla(260, 100%, 88%, 1); // #DACEF3
$ui-purple: hsla(260, 100%, 70%, 1); // #9966FF Looks Primary
$ui-purple-dark: hsla(260, 60%, 60%, 1); // #855CD6 Looks Secondary
$ui-purple-darker: hsla(260, 46%, 54%, 1);

View file

@ -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
};

View file

@ -0,0 +1,26 @@
.playlist {
display: flex;
flex-direction: column;
gap: 1rem;
.playlist-title {
display: flex;
font-size: 2rem;
font-weight: 700;
line-height: 2.5rem;
text-align: start;
}
.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;
}
}

View 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
};

View file

@ -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;
}
}

View 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
};

View 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;
}

View file

@ -4,12 +4,15 @@ const {useState, useCallback} = require('react');
const Button = require('../../components/forms/button.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const TitleBanner = require('../../components/title-banner/title-banner.jsx');
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');
const {CardsModal} = require('../../components/cards-modal/cards-modal.jsx');
require('./ideas.scss');
@ -87,46 +90,51 @@ const physicalIdeasData = [
}
];
const playlists = {
'sprites-and-vectors': 'ideas.spritesAndVector',
'tips-and-tricks': 'ideas.tipsAndTricks',
'advanced-topics': 'ideas.advancedTopics'
};
const Ideas = () => {
const intl = useIntl();
const [isOpen, setIsOpen] = useState(false);
const [youtubeVideoId, setYoutubeVideoId] = useState('');
const [isCardsModalOpen, setCardsModalOpen] = useState(false);
const onOpen = useCallback(() => setIsOpen(true), [setIsOpen]);
const onClose = useCallback(() => setIsOpen(false), [setIsOpen]);
const onCloseVideoModal = useCallback(() => setYoutubeVideoId(''), [setYoutubeVideoId]);
const onSelectedVideo = useCallback(
videoId => setYoutubeVideoId(videoId),
[setYoutubeVideoId]
);
const onCardsModalOpen = useCallback(() => setCardsModalOpen(true), [isCardsModalOpen]);
const onCardsModalClose = useCallback(() => setCardsModalOpen(false), [isCardsModalOpen]);
return (
<div>
<div className="banner-wrapper">
<TitleBanner className="masthead ideas-banner">
<div className="title-banner-p">
<img
alt={intl.formatMessage({id: 'ideas.headerImageDescription'})}
src="/images/ideas/masthead-illustration.svg"
src="/images/ideas/banner.svg"
/>
<h1 className="title-banner-h1">
<FormattedMessage id="ideas.headerMessage" />
</h1>
<a href="/projects/editor/?tutorial=all">
<Button className="banner-button">
<img
alt=""
src="/images/ideas/bulb-yellow-icon.svg"
/>
<FormattedMessage id="ideas.headerButtonMessage" />
</Button>
</a>
<div className="banner-description">
<div className="title">
<FormattedMessage id="ideas.headerTitle" />
</div>
<p>
<FormattedMessage
id="ideas.headerDescription"
values={{a: chunks => <a href="https://scratch.mit.edu/projects/1093752362/">{chunks}</a>}}
/>
</p>
</div>
</TitleBanner>
</div>
<div className="tips">
<div className="inner">
<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}
@ -160,17 +168,51 @@ 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
className="download-cards"
>
<Button
className="pass"
onClick={onOpen}
onClick={onCardsModalOpen}
>
<img src="/images/ideas/download-icon.svg" />
</Button>
@ -178,12 +220,12 @@ const Ideas = () => {
id="ideas.downloadGuides"
values={{
strong: chunks => <strong>{chunks}</strong>,
a: chunks => <a onClick={onOpen}>{chunks}</a>
a: chunks => <a onClick={onCardsModalOpen}>{chunks}</a>
}}
/>
<CardsModal
isOpen={isOpen}
onClose={onClose}
isOpen={isCardsModalOpen}
onClose={onCardsModalClose}
/>
</div>
</div>
@ -231,7 +273,9 @@ const Ideas = () => {
}
/>
<FormattedMessage
id={physicalIdea.physicalIdeasDescription.buttonTextId}
id={
physicalIdea.physicalIdeasDescription.buttonTextId
}
/>
</Button>
</a>

View file

@ -9,12 +9,40 @@ $base-bg: $ui-white;
}
.banner-wrapper {
background: $ui-aqua bottom right url("/images/ideas/right-juice.png") no-repeat;
}
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 2rem;
padding: 3rem 0;
background-color: $ui-aqua;
.ideas-banner {
margin-bottom: 0;
background: bottom left url("/images/ideas/left-juice.png") no-repeat;
.banner-description {
display: flex;
flex-direction: column;
text-align: start;
max-width: 27rem;
.title {
font-size: 2rem;
font-weight: 700;
line-height: 2.5rem;
color: $ui-white;
}
p {
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
color: $ui-white;
a {
font-weight: 700;
text-decoration: underline;
color: $ui-purple-light;
}
}
}
}
.tips {
@ -24,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;
@ -42,6 +72,52 @@ $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;
}
.download-cards {
display: flex;
align-items: center;
@ -63,6 +139,7 @@ $base-bg: $ui-white;
text-align: start;
a {
font-weight: 400;
text-decoration: underline;
}
}
@ -154,10 +231,14 @@ $base-bg: $ui-white;
}
}
.section-header {
.tips, .physical-ideas {
.section-header {
display: flex;
font-size: 2rem;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
font-weight: 700;
line-height: 2.5rem;
text-align: start;
}
}
.tips-button {
@ -166,7 +247,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;
@ -178,29 +258,16 @@ $base-bg: $ui-white;
//4 columns
@media #{$small} {
.title-banner {
&.masthead {
padding-bottom: 1.25rem;
p {
max-width: $cols4;
}
.banner-wrapper {
img {
display: none;
}
}
// put the image first if in 4-column
.tips-info-body {
max-width: $cols4;
text-align: center;
&.tips-illustration {
order: -1;
img {
width: $cols4;
}
}
.button {
width: 100%;
}
@ -209,34 +276,14 @@ $base-bg: $ui-white;
//6 columns
@media #{$medium} {
.title-banner {
&.masthead {
p {
max-width: $cols6;
}
}
}
.tips-info-body {
max-width: $cols4;
text-align: center;
}
}
//8 columns
@media #{$intermediate} {
.title-banner {
&.masthead {
padding-bottom: 2rem;
p {
max-width: $cols6;
}
}
}
.tips-info-body {
max-width: $cols4;
}
@ -256,16 +303,6 @@ $base-bg: $ui-white;
// 12 columns
@media #{$big} {
.title-banner {
&.masthead {
padding-bottom: 1.25rem;
p {
max-width: $cols8;
}
}
}
.tips-info-section {
&.mod-align-top {
align-items: flex-start;

View file

@ -1,6 +1,7 @@
{
"ideas.headerMessage": "What will you create?",
"ideas.headerImageDescription": "Outlandish creations from pixelated unicorns to drumbeat waveforms to levitating tacos to buckets of rainbows.",
"ideas.headerTitle": "Looking for a project idea?",
"ideas.headerDescription": "Try Scratchs Project Idea Generator! Pick as many ideas as youd like. Mix and match ideas! <a>Remix your own</a> idea generator! The possibilities are endless.",
"ideas.headerImageDescription": "Scratch cat holding a lightning bulb and a block",
"ideas.headerButtonMessage": "Choose a tutorial",
"ideas.startHereText": "New to Scratch? Start here!",
"ideas.gettingStartedButtonText": "Try Getting Started Tutorial",
@ -31,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.",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 370 KiB

View 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

View 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

View file

@ -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'}"`,