Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 23 additions & 63 deletions apps/src/sites/studio/pages/sections/show.js
Original file line number Diff line number Diff line change
@@ -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(
<SectionSignIn {...data} />,
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();
}
186 changes: 186 additions & 0 deletions apps/src/templates/sections/SectionSignIn.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.container}>
<MuiTypography variant="h2" gutterBottom>
{welcome}
</MuiTypography>

<form
action={submitPath}
method="post"
className={styles.form}
onSubmit={() => clientState.reset()}
>
<input
type="hidden"
name="authenticity_token"
value={authenticityToken}
/>
<input type="hidden" name="user_id" value={userId || ''} />
<input type="hidden" name="secret_picture_id" value={pictureId || ''} />

<MuiTypography variant="h4" component="h2">
{nameInstruction}
</MuiTypography>
<Chips
name="student_name_chip"
options={students.map(student => ({
value: String(student.id),
label: student.name,
}))}
values={userId !== null ? [String(userId)] : []}
setValues={onSelectName}
/>

{nameChosen && isPicture && (
<div className={styles.secret}>
<MuiTypography variant="h4" component="h2">
{pictureInstruction}
</MuiTypography>
<ul className={styles.tiles}>
{secretPictures.map(picture => (
<li key={picture.id}>
<button
type="button"
className={classNames(styles.tile, {
[styles.tileSelected]: pictureId === picture.id,
})}
onClick={() => setPictureId(picture.id)}
>
<img src={picture.path} width={66} alt={picture.name} />
</button>
</li>
))}
</ul>
</div>
)}

{nameChosen && isWord && (
<div className={styles.secret}>
<MuiTypography variant="h4" component="h2">
{wordInstruction}
</MuiTypography>
<TextField
name="secret_words"
value={secretWords}
onChange={e => setSecretWords(e.target.value)}
autoComplete="off"
className={styles.wordInput}
/>
</div>
)}

{showPairing && (
<Checkbox
name="show_pairing_dialog"
value="1"
checked={pairing}
onChange={e => setPairing(e.target.checked)}
label={pairProgramming}
/>
)}

{nameChosen && (
<MuiButton
type="submit"
variant="contained"
color="primary"
disabled={!secretChosen}
>
{loginLabel}
</MuiButton>
)}
</form>
</div>
);
}

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,
};
54 changes: 54 additions & 0 deletions apps/src/templates/sections/section-sign-in.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
40 changes: 0 additions & 40 deletions dashboard/app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading