Skip to content
Merged
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions docs/classroom/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Playwright MCP および Selenium integration tests で使用する `data-testid
| `classroom-back` | button | 戻る |
| `classroom-refresh` | button | 更新 (↻) |
| `classroom-google-import` | button | Google Classroom からインポート |
| `classroom-teacher-logout` | button | ログアウト |
| `classroom-avatar-button` | button | アカウントメニュー(タイトルバー右上・メール頭文字 + ▼) |
| `classroom-teacher-logout` | button | ログアウト(アカウントメニュー内) |

### クラス作成

Expand Down Expand Up @@ -204,7 +205,8 @@ Playwright MCP および Selenium integration tests で使用する `data-testid
| `classroom-class-open-{groupId}` | button | クラスカード本体(クリックでクラスをひらく) |
| `classroom-class-evaluate-{groupId}` | button | クラスカードの「評価」 |
| `classroom-class-import-gc` | button | クラス一覧の「Google Classroom からインポート」(Google ログイン時のみ) |
| `classroom-teacher-logout` | button | ログアウト(タイトルバー右端に常時表示) |
| `classroom-avatar-button` / `classroom-avatar-initials` / `classroom-avatar-email` | button/span | アカウントメニュー(右上・メール頭文字 + ▼。クリックでメール表示 + ログアウト) |
| `classroom-teacher-logout` | button | ログアウト(アカウントメニュー内。#1111 レビューでタイトルバー常時表示から移動) |
| `classroom-class-create-section` | input | 同時作成: セクション(オプション) |
| `classroom-class-settings-open-{groupId}` | button | クラスカードの「設定」(インライン編集を開く) |
| `classroom-class-settings-{groupId}` | form | クラス設定のインライン編集フォーム |
Expand Down Expand Up @@ -294,7 +296,7 @@ Playwright MCP および Selenium integration tests で使用する `data-testid
| `classroom-description-preview` | div | 右ペインの生徒視点プレビュー |
| `classroom-description-preview-body` | div | プレビュー本文(テキスト+画像) |
| `classroom-description-preview-prev` / `-next` | button | プレビューのページ送り |
| `classroom-teacher-email` | span | タイトルバーのユーザーメール(OIDC トークンに email がある場合のみ) |
| `classroom-avatar-email` | span | アカウントメニュー内のユーザーメール(OIDC トークンに email がある場合のみ。旧 `classroom-teacher-email` は廃止) |
| `classroom-board-create-cancel` / `classroom-board-reuse-cancel` | button | インラインフォームのキャンセル |
| `classroom-google-course-imported-{courseId}` | span | GC コースの「インポート済み」バッジ |
| `classroom-breadcrumb-assignments` | button | パンくず「課題一覧」リンク |
Expand Down
26 changes: 18 additions & 8 deletions docs/classroom/ui-ux.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,25 +165,35 @@ Google または Microsoft アカウントでサインインする画面。先
- 課題の所属クラス変更・人数編集・課題単位の共同管理者・複製は**できない**(クラス設定 / 課題一覧の再利用へ集約)。フッターのボタンは「**課題をアーカイブ**」(soft-delete。ボードの「アーカイブ済みの課題」からいつでも復元可能。testid は歴史的経緯で `classroom-delete-classroom` のまま)
- 主な data-testid: `classroom-tab-description` / `classroom-description-editor` / `classroom-description-preview[-body|-prev|-next]` / `classroom-tab-members`

## 1.8 先生: お知らせセンター(タイトルバー 🔔・EPIC #1111)
## 1.8 先生: お知らせセンター(タイトルバー・EPIC #1111)

![お知らせセンター](screenshots/0218-teacher-notifications.png)

クラス管理のタイトルバー右上(× の左隣)に 🔔 ボタン。運営(Admin)からのお知らせが届くと未読数バッジが付く。
クラス管理のタイトルバー右上(× の左隣)は **アバターメニュー**(メール頭文字 + ▼)に固定。その左に **白一色のベル**(お知らせ)を置く。運営(Admin)からのお知らせが届くと未読数バッジが付く。

- クリックで一覧パネルを開閉。**開いた時点で全既読**になりバッジが消える(その場では未読ドットで新着を見分けられる)
- お知らせ本文をクリックすると、`link` の種類に応じて該当画面へジャンプ(`kind: 'classroom'` → そのクラスを選択して課題詳細へ)。未知の kind は無視(前方互換)
- 60 秒間隔でポーリング。取得エラーはクラス管理本体に影響させない(表示しない)
- **アバターメニュー**: メール頭文字(`kouji@…`→`K` / `kouji.takao@…`→`KT`)の丸アイコン + ▼。クリックでポップアップ(メール表示 + ログアウト。将来の設定項目もここに集約)。紫背景で視認できる白丸 + 紫文字
- ベルのクリックで一覧パネルを開閉。**開いた時点で全既読**になりバッジが消える(その場では未読ドットで新着を見分けられる)
- パネルは **先頭 5 件**のみ表示。6 件以上あるときは「すべて見る(N件)」で **全件一覧ページ**(`teacher-notifications` フェーズ・**10 件/ページ**)へ遷移
- お知らせ本文をクリックすると、`link` の種類に応じて該当画面へジャンプ(`kind: 'classroom'` → そのクラスを選択して課題詳細へ / `kind: 'shared-mine'` → みんなの課題の自分の投稿へ)。未知の kind は無視(前方互換)
- **取得は 1 日 1 回**(コスト削減): その日はじめてクラス管理を開いたときだけ `GET /notifications` を 1 回呼び、localStorage に日付つきでキャッシュ。同じ日の再オープンは API を叩かない(旧 60 秒ポーリングは廃止)。共有 PC 対策として先生メールでキャッシュを識別。取得エラーはクラス管理本体に影響させない(表示しない)。※その日の初回取得後に届いたお知らせは翌日反映(運営連絡は多くて週 1 回程度の前提)
- 送信側は Admin SPA(クラス詳細の「先生へのお知らせ」フォーム → `POST /admin/notifications`)。`docs/admin/README.md` を参照

| 要素 | data-testid | 操作 |
|------|-------------|------|
| 🔔 ボタン | `classroom-notifications-button` | クリックでパネル開閉 |
| アバターボタン | `classroom-avatar-button` | クリックでアカウントメニュー開閉 |
| イニシャル丸 | `classroom-avatar-initials` | メール頭文字(1〜2 文字) |
| メニューのメール | `classroom-avatar-email` | ポップアップ内のメール表示 |
| ログアウト | `classroom-teacher-logout` | アカウントメニュー内 |
| ベルボタン | `classroom-notifications-button` | クリックでパネル開閉 |
| 未読バッジ | `classroom-notifications-badge` | 未読数(10 以上は「9+」)。未読 0 で非表示 |
| 一覧パネル | `classroom-notifications-panel` | — |
| お知らせ 1 件 | `classroom-notification-item-{notificationId}` | クリックでリンク先へジャンプ + パネルを閉じる |
| 一覧パネル | `classroom-notifications-panel` | 先頭 5 件 |
| すべて見る | `classroom-notifications-see-all` | 全件一覧ページへ(6 件以上のとき) |
| お知らせ 1 件(パネル) | `classroom-notification-item-{notificationId}` | クリックでリンク先へジャンプ + パネルを閉じる |
| 未読ドット | `classroom-notification-unread-dot` | 未読アイテムのみ |
| 空メッセージ | `classroom-notifications-empty` | お知らせ 0 件のとき |
| 全件一覧ページ | `classroom-notifications-page` | `teacher-notifications` フェーズ |
| ページ内 1 件 | `classroom-notification-page-item-{notificationId}` | クリックでリンク先へジャンプ |
| ページャ | `classroom-notifications-pager` / `-prev` / `-next` | 10 件/ページ |

## 1.9 先生: 共有推奨バナー(#1106)

Expand Down
75 changes: 73 additions & 2 deletions packages/admin/src/components/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,81 @@ body {
font-size: 0.75rem;
}

.admin-header-email {

/* アバターメニュー(右上固定・#1111 レビュー)。 */
.admin-avatar-menu {
position: relative;
margin-left: auto;
}

.admin-avatar-button {
display: flex;
align-items: center;
gap: 0.25rem;
border: none;
background: transparent;
padding: 0;
cursor: pointer;
}

.admin-avatar-initials {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.9rem;
height: 1.9rem;
border-radius: 50%;
background: white;
color: hsl(260, 60%, 50%);
font-size: 0.75rem;
font-weight: bold;
}

.admin-avatar-caret {
color: white;
font-size: 0.6rem;
line-height: 1;
}

.admin-avatar-popup {
position: absolute;
top: 2.6rem;
right: 0;
min-width: 13rem;
background: white;
border: 1px solid hsl(220, 15%, 85%);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
color: hsl(220, 15%, 25%);
text-align: left;
overflow: hidden;
z-index: 100;
}

.admin-avatar-popup-email {
padding: 0.6rem 0.9rem;
font-size: 0.75rem;
color: hsl(220, 10%, 45%);
border-bottom: 1px solid hsl(220, 15%, 92%);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.admin-avatar-popup-item {
display: block;
width: 100%;
padding: 0.6rem 0.9rem;
border: none;
background: none;
text-align: left;
font-size: 0.85rem;
opacity: 0.9;
color: inherit;
cursor: pointer;
}

.admin-avatar-popup-item:hover {
background: hsl(220, 20%, 96%);
}

.admin-main {
Expand Down
16 changes: 12 additions & 4 deletions packages/admin/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {useCallback, useEffect, useRef, useState} from 'react';
import {fetchMe, setIdToken} from '../lib/admin-api.js';
import {getDevLoginToken, initGoogleSignIn} from '../lib/google-auth.js';
import AvatarMenu from './avatar-menu.jsx';
import BugReportsView from './bug-reports-view.jsx';
import ClassroomsView from './classrooms-view.jsx';
import SharedAssignmentsView from './shared-assignments-view.jsx';
Expand Down Expand Up @@ -41,6 +42,13 @@ const App = () => {

const handleReload = useCallback(() => window.location.reload(), []);

// トークンはモジュールメモリのみ。ログアウトはリロードで確実に破棄し
// サインイン画面へ戻す(session-expired の再読み込みと同じ手当て)。
const handleLogout = useCallback(() => {
setIdToken(null);
window.location.reload();
}, []);

// Google ID tokens live ~1 hour and the SPA keeps them in memory only, so
// an expired session surfaces as API 401s. The clients broadcast those;
// show one clear prompt instead of per-view raw errors.
Expand Down Expand Up @@ -91,10 +99,10 @@ const App = () => {
<header className="admin-header">
<h1>{'Smalruby Admin'}</h1>
<span className="admin-header-stage">{me.stage}</span>
<span
className="admin-header-email"
data-testid="admin-me-email"
>{me.email}</span>
<AvatarMenu
email={me.email}
onLogout={handleLogout}
/>
</header>
<nav className="admin-nav">
{SECTIONS.map(s => (
Expand Down
92 changes: 92 additions & 0 deletions packages/admin/src/components/avatar-menu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* アバターメニュー(管理 SPA ヘッダー右上・#1111 レビュー)。
*
* 右上はアカウント(メール頭文字)に固定し、クリックでポップアップ
* (メール表示 + ログアウト。将来の設定項目もここに足す)。teacher 側の
* TeacherAvatarMenu と同じ挙動を admin のスタイルで実装。
*/
import PropTypes from 'prop-types';
import {useCallback, useEffect, useRef, useState} from 'react';
import {initialsFromEmail} from '../lib/avatar-initials.js';

const AvatarMenu = ({email, onLogout}) => {
const [open, setOpen] = useState(false);
const wrapperRef = useRef(null);

const toggle = useCallback(() => setOpen(o => !o), []);
const handleLogout = useCallback(() => {
setOpen(false);
onLogout();
}, [onLogout]);

useEffect(() => {
if (!open) return () => {};
const onDocDown = e => {
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
};
const onKey = e => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', onDocDown);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDocDown);
document.removeEventListener('keydown', onKey);
};
}, [open]);

return (
<div
className="admin-avatar-menu"
ref={wrapperRef}
>
<button
aria-expanded={open}
aria-haspopup="menu"
aria-label="アカウントメニュー"
className="admin-avatar-button"
data-testid="admin-avatar-button"
title={email || ''}
type="button"
onClick={toggle}
>
<span
className="admin-avatar-initials"
data-testid="admin-avatar-initials"
>{initialsFromEmail(email)}</span>
<span
aria-hidden="true"
className="admin-avatar-caret"
>{'▼'}</span>
</button>
{open && (
<div
className="admin-avatar-popup"
data-testid="admin-avatar-popup"
role="menu"
>
{email ? (
<div
className="admin-avatar-popup-email"
data-testid="admin-avatar-email"
>{email}</div>
) : null}
<button
className="admin-avatar-popup-item"
data-testid="admin-logout"
role="menuitem"
type="button"
onClick={handleLogout}
>{'ログアウト'}</button>
</div>
)}
</div>
);
};

AvatarMenu.propTypes = {
email: PropTypes.string,
onLogout: PropTypes.func.isRequired
};

export default AvatarMenu;
25 changes: 25 additions & 0 deletions packages/admin/src/lib/avatar-initials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* メールアドレスからアバター用イニシャルを作る(#1111 レビュー)。
* scratch-gui 側 (src/lib/avatar-initials.js) と同じ仕様の複製
* (別アプリのため共有できない。仕様変更時は両方を更新する)。
*
* - `kouji@example.com` → `K`
* - `kouji.takao@example.com` → `KT`
* - `kouji.takao.xxx@example.com` → `KT`
* @param {string} email - メールアドレス(null/不正でも安全)
* @returns {string} 1〜2 文字のイニシャル(大文字)
*/
const initialsFromEmail = email => {
if (typeof email !== 'string' || email.indexOf('@') < 1) return '?';
const local = email.slice(0, email.indexOf('@'));
const segments = local.split('.').filter(s => s.length > 0);
if (segments.length === 0) return '?';
const first = segments[0][0];
if (segments.length >= 2) {
return (first + segments[1][0]).toUpperCase();
}
return first.toUpperCase();
};

export {initialsFromEmail};
export default initialsFromEmail;
5 changes: 4 additions & 1 deletion packages/admin/test/unit/app.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ describe('App auth flow', () => {

render(<App />);
await waitFor(() => expect(screen.getByTestId('admin-dashboard')).toBeInTheDocument());
expect(screen.getByTestId('admin-me-email')).toHaveTextContent('admin@example.com');
// 右上はアバター(メール頭文字)。メールはメニューを開くと出る(#1111)。
expect(screen.getByTestId('admin-avatar-initials')).toHaveTextContent('A');
fireEvent.click(screen.getByTestId('admin-avatar-button'));
expect(screen.getByTestId('admin-avatar-email')).toHaveTextContent('admin@example.com');
expect(mockSetIdToken).toHaveBeenCalledWith('dev-token');
});

Expand Down
4 changes: 4 additions & 0 deletions packages/scratch-gui/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ src/lib/*
!src/lib/koshien-mock-config.js
!src/lib/koshien-test-url.js
!src/lib/deck-setup.js
!src/lib/avatar-initials.js
!src/lib/classroom-api.js
!src/lib/classroom-assignment-utils.js
!src/lib/classroom-assignment-templates/
Expand Down Expand Up @@ -326,6 +327,8 @@ test/unit/components/*
!test/unit/components/student-assignment-panel.test.jsx
!test/unit/components/teacher-class-detail.test.jsx
!test/unit/components/teacher-notifications.test.jsx
!test/unit/components/teacher-avatar-menu.test.jsx
!test/unit/components/teacher-notifications-list.test.jsx
!test/unit/components/classroom-teacher-modal-share-suggestion.test.js
!test/unit/components/mobile-bottom-tabs.test.jsx
!test/unit/components/mobile-drawer.test.jsx
Expand Down Expand Up @@ -382,6 +385,7 @@ test/unit/lib/*
!test/unit/lib/koshien-test-url.test.js
!test/unit/lib/blockly-private-api.test.js
!test/unit/lib/blocks-gesture-recovery.test.js
!test/unit/lib/avatar-initials.test.js
!test/unit/lib/classroom-api.test.js
!test/unit/lib/classroom-assignment-templates.test.js
!test/unit/lib/classroom-assignment-utils.test.js
Expand Down
Loading
Loading