diff --git a/apps/src/sites/studio/pages/sections/show.js b/apps/src/sites/studio/pages/sections/show.js
index 04a7c1583120f..91b005bf5327d 100644
--- a/apps/src/sites/studio/pages/sections/show.js
+++ b/apps/src/sites/studio/pages/sections/show.js
@@ -1,63 +1,23 @@
-import clientState from '@cdo/apps/code-studio/clientState.js';
-
-const script = document.querySelector('script[data-section]');
-const sectionData = JSON.parse(script.dataset['section']);
-const {loginType, loginTypeWord, loginTypePicture, pairingAllowed} =
- sectionData;
-
-$(function () {
- // Select name.
- $('ul.students li').click(function () {
- $('ul.students li').removeClass('selected');
- $(this).addClass('selected');
- $('input#user_id').val($(this).attr('id'));
-
- if (loginType === loginTypeWord) {
- // Clear the password.
- $('#secret_words').val('');
- } else if (loginType === loginTypePicture) {
- // Deselect picture.
- $('ul.pictures li').removeClass('selected');
- }
-
- // Disable the login button.
- $('#login_button').prop('disabled', true);
-
- // Hide the pairing checkbox
- $('#pairing_checkbox').hide();
-
- // Reveal the secret section...
- $('#secret').hide().slideDown();
-
- // ...and simultaneously fade in the login button.
- $('#login_button').fadeIn();
- });
-
- // Select secret picture.
- $('ul.pictures li').click(function () {
- $('ul.pictures li').removeClass('selected');
- $(this).addClass('selected');
- $('input#secret_picture_id').val($(this).attr('id'));
-
- // Show the pairing checkbox
- if (pairingAllowed) {
- $('#pairing_checkbox').show();
- }
-
- // Enable the login button.
- $('#login_button').prop('disabled', false);
- });
-
- // Type something in password box.
- $('#secret_words').keyup(function () {
- // Show the pairing checkbox
- if (pairingAllowed) {
- $('#pairing_checkbox').show();
- }
-
- // Enable the login button.
- $('#login_button').prop('disabled', false);
- });
-
- $('.section-user-sign-in').on('submit', clientState.reset);
-});
+import React from 'react';
+
+import SectionSignIn from '@cdo/apps/templates/sections/SectionSignIn';
+import {createReactRoot} from '@cdo/apps/util/createReactRoot';
+
+function mount() {
+ const script = document.querySelector('script[data-section]');
+ const data = JSON.parse(script.dataset.section);
+
+ createReactRoot(
+ ,
+ document.getElementById('section-sign-in'),
+ {legacyReactDomRender: true}
+ );
+}
+
+// The entry script tag precedes the mount-point div in the view, so wait for
+// the document to finish parsing before looking it up.
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', mount);
+} else {
+ mount();
+}
diff --git a/apps/src/templates/sections/SectionSignIn.jsx b/apps/src/templates/sections/SectionSignIn.jsx
new file mode 100644
index 0000000000000..14e3df47aec80
--- /dev/null
+++ b/apps/src/templates/sections/SectionSignIn.jsx
@@ -0,0 +1,186 @@
+import Checkbox from '@code-dot-org/component-library/checkbox';
+import Chips from '@code-dot-org/component-library/chips';
+import TextField from '@code-dot-org/component-library/textField';
+import {Typography as MuiTypography, Button as MuiButton} from '@mui/material';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React, {useState} from 'react';
+
+import clientState from '@cdo/apps/code-studio/clientState';
+
+import styles from './section-sign-in.module.scss';
+
+/**
+ * Design-system version of the student "sign in to a section" page
+ * (sections#show). Students pick their name, then a secret picture or word, and
+ * submit. Built from DSCO components; the form is still a native POST (with the
+ * Rails CSRF token) so the server-side authentication flow is unchanged. The
+ * translated copy, student/picture data, CSRF token, and submit path are
+ * supplied by the server.
+ */
+export default function SectionSignIn({
+ submitPath,
+ authenticityToken,
+ welcome,
+ nameInstruction,
+ pictureInstruction,
+ wordInstruction,
+ pairProgramming,
+ loginLabel,
+ students,
+ secretPictures,
+ loginType,
+ loginTypePicture,
+ loginTypeWord,
+ pairingAllowed,
+}) {
+ const [userId, setUserId] = useState(null);
+ const [pictureId, setPictureId] = useState(null);
+ const [secretWords, setSecretWords] = useState('');
+ const [pairing, setPairing] = useState(false);
+
+ const isPicture = loginType === loginTypePicture;
+ const isWord = loginType === loginTypeWord;
+
+ const nameChosen = userId !== null;
+ let secretChosen = false;
+ if (isPicture) {
+ secretChosen = pictureId !== null;
+ } else if (isWord) {
+ secretChosen = secretWords.trim().length > 0;
+ }
+ const showPairing = pairingAllowed && secretChosen;
+
+ // The DSCO Chips group is multi-select; drive it as single-select by keeping
+ // at most one value. `setValues` receives the next array (the clicked chip
+ // added, or removed when the current one is clicked again). Picking a
+ // (different) name resets the secret-picture/word selection, per the legacy
+ // flow.
+ const onSelectName = nextValues => {
+ const added = nextValues.find(value => value !== String(userId));
+ setUserId(added ? Number(added) : null);
+ setPictureId(null);
+ setSecretWords('');
+ setPairing(false);
+ };
+
+ return (
+
+ );
+}
+
+SectionSignIn.propTypes = {
+ submitPath: PropTypes.string.isRequired,
+ authenticityToken: PropTypes.string.isRequired,
+ welcome: PropTypes.string.isRequired,
+ nameInstruction: PropTypes.string.isRequired,
+ pictureInstruction: PropTypes.string,
+ wordInstruction: PropTypes.string,
+ pairProgramming: PropTypes.string.isRequired,
+ loginLabel: PropTypes.string.isRequired,
+ students: PropTypes.arrayOf(
+ PropTypes.shape({id: PropTypes.number, name: PropTypes.string})
+ ).isRequired,
+ secretPictures: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number,
+ path: PropTypes.string,
+ name: PropTypes.string,
+ })
+ ),
+ loginType: PropTypes.string.isRequired,
+ loginTypePicture: PropTypes.string.isRequired,
+ loginTypeWord: PropTypes.string.isRequired,
+ pairingAllowed: PropTypes.bool,
+};
diff --git a/apps/src/templates/sections/section-sign-in.module.scss b/apps/src/templates/sections/section-sign-in.module.scss
new file mode 100644
index 0000000000000..8e86e3d73f590
--- /dev/null
+++ b/apps/src/templates/sections/section-sign-in.module.scss
@@ -0,0 +1,54 @@
+.container {
+ max-width: 960px;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+}
+
+.tiles {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+// Picture-login tiles: image thumbnails.
+.tile {
+ border: 1px solid var(--borders-neutral-primary);
+ border-radius: 5px;
+ background-color: var(--background-neutral-secondary);
+ padding: 5px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background-color: var(--background-neutral-tertiary);
+ }
+
+ img {
+ display: block;
+ }
+}
+
+.tileSelected,
+.tileSelected:hover {
+ background-color: var(--background-brand-teal-light);
+}
+
+.secret {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.wordInput {
+ max-width: 320px;
+}
diff --git a/dashboard/app/assets/stylesheets/application.scss b/dashboard/app/assets/stylesheets/application.scss
index 23e5b2c852d3e..6d9216a71aee2 100644
--- a/dashboard/app/assets/stylesheets/application.scss
+++ b/dashboard/app/assets/stylesheets/application.scss
@@ -1865,46 +1865,6 @@ a.download-video {
margin: 10px 0;
}
-#signinsection {
- ul.students, ul.pictures {
- margin-left: 0;
- }
-
- ul.students li,
- ul.pictures li {
- list-style-type: none;
- border: 1px solid $lighter_gray;
- border-radius: 5px;
- background-color: $lightest_gray;
- float: left;
- margin: 5px;
- padding: 5px;
- cursor: pointer;
- }
-
- ul.students li:hover,
- ul.pictures li:hover {
- background-color: $light_yellow;
- }
-
- ul.students li.selected,
- ul.pictures li.selected {
- background-color: $yellow;
- }
-
- #pairing_checkbox {
- margin: 5px 0;
- label {
- font-size: 17.5px;
- }
- label, input {
- margin: 5px;
- display: inline-block;
- vertical-align: middle;
- }
- }
-}
-
.flex-container {
display: flex;
justify-content: space-between;
diff --git a/dashboard/app/views/sections/show.html.haml b/dashboard/app/views/sections/show.html.haml
index e9cf79e885007..25911bb27d6d1 100644
--- a/dashboard/app/views/sections/show.html.haml
+++ b/dashboard/app/views/sections/show.html.haml
@@ -1,65 +1,29 @@
- @page_title = @section.name
:ruby
+ picture_login = @section.login_type == Section::LOGIN_TYPE_PICTURE
section_data = {
+ submitPath: log_in_section_path(id: @section.code),
+ authenticityToken: form_authenticity_token,
+ welcome: t('signinsection.welcome', section_name: @section.name),
+ nameInstruction: "#{t('signinsection.name')}*",
+ pictureInstruction: t('signinsection.picture'),
+ wordInstruction: t('signinsection.words'),
+ pairProgramming: t('signinsection.pair_programming'),
+ loginLabel: t('signinsection.login'),
loginType: @section.login_type,
loginTypePicture: Section::LOGIN_TYPE_PICTURE,
loginTypeWord: Section::LOGIN_TYPE_WORD,
- pairingAllowed: @section.pairing_allowed
+ pairingAllowed: @section.pairing_allowed,
+ students: @section.name_safe_students.map {|student| {id: student.id, name: student.name}},
+ secretPictures: picture_login ? @secret_pictures.map {|p| {id: p.id, path: image_path(p.path), name: p.name}} : []
}.to_json
%script{src: webpack_asset_path('js/sections/show.js'), data: {section: section_data}}
-#signinsection
- %h2
- =t('signinsection.welcome', section_name: @section.name)
+#section-sign-in
- %div#user
- %h4.instructions
- = succeed '*' do
- = t('signinsection.name')
- %ul.students
- - @section.name_safe_students.each do |student|
- %li{id: student.id}
- = student.name
- .clear
-
- = form_tag(log_in_section_path(id: @section.code), class: 'section-user-sign-in') do
- = hidden_field_tag :secret_picture_id
- = hidden_field_tag :user_id
-
- - if @section.login_type == Section::LOGIN_TYPE_PICTURE
- #secret{style: 'display: none;'}
- %h4.instructions= t('signinsection.picture')
- %ul.pictures
- - @secret_pictures.each do |secret_picture|
- %li{id: secret_picture.id}
- = image_tag secret_picture.path, width: 66, alt: secret_picture.name
- .clear
-
- - if @section.login_type == Section::LOGIN_TYPE_WORD
- #secret{style: 'display: none;'}
- %h4.instructions= t('signinsection.words')
- = text_field_tag :secret_words, nil, autocomplete: 'off'
- .clear
-
- #pairing_checkbox{style: 'display: none'}
- = check_box_tag :show_pairing_dialog
- = label_tag :show_pairing_dialog, t('signinsection.pair_programming')
-
- = button_tag t('signinsection.login'),
- id: 'login_button',
- style: 'display:none',
- class: 'btn btn-primary',
- data: {disable_with: I18n.t('signinsection.signing_in')}
-
- %br/
- %br/
- %div.span{ :style => "margin-left: auto"}
- != "* "
- %span
- != t('signinsection.student_privacy_markdown', student_privacy_blog: 'https://code.org/privacy', markdown: true)
-
-%br/
-%br/
-%br/
+%div.span{style: 'margin-left: auto'}
+ != "* "
+%span
+ != t('signinsection.student_privacy_markdown', student_privacy_blog: 'https://code.org/privacy', markdown: true)