Add user menu with profile page, card and row hovers, and family breadcrumb fixes
This commit is contained in:
+1
-5
@@ -56,11 +56,7 @@ The capture is the visible viewport, not the full page, so click coordinates rea
|
|||||||
|
|
||||||
Invocations must not linger: every run exits by itself within its 15-second internal timeout, leaving the browser and tab untouched.
|
Invocations must not linger: every run exits by itself within its 15-second internal timeout, leaving the browser and tab untouched.
|
||||||
|
|
||||||
At the end of a capture session, quit the browser:
|
Leave the browser running between capture sessions — never kill it. In practice logins do not reliably survive a browser restart, and killing it forces a human to sign in again.
|
||||||
|
|
||||||
pkill -f capture-profile
|
|
||||||
|
|
||||||
The login session persists in the profile, so the next `tools/capturebrowser` launch is still signed in.
|
|
||||||
|
|
||||||
## Agent recipe
|
## Agent recipe
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func Register(mux *http.ServeMux, cache *Cache) {
|
|||||||
for _, section := range sections {
|
for _, section := range sections {
|
||||||
mux.HandleFunc("GET /"+section, a.page)
|
mux.HandleFunc("GET /"+section, a.page)
|
||||||
}
|
}
|
||||||
|
mux.HandleFunc("GET /profile", a.page)
|
||||||
mux.HandleFunc("GET /people/{email}", a.page)
|
mux.HandleFunc("GET /people/{email}", a.page)
|
||||||
mux.HandleFunc("GET /families/{key}", a.page)
|
mux.HandleFunc("GET /families/{key}", a.page)
|
||||||
mux.HandleFunc("GET /classrooms/{name}", a.page)
|
mux.HandleFunc("GET /classrooms/{name}", a.page)
|
||||||
@@ -58,6 +59,7 @@ func (a app) page(w http.ResponseWriter, r *http.Request) {
|
|||||||
data := map[string]string{
|
data := map[string]string{
|
||||||
"UserName": name,
|
"UserName": name,
|
||||||
"UserInitial": strings.ToUpper(name[:1]),
|
"UserInitial": strings.ToUpper(name[:1]),
|
||||||
|
"UserEmail": auth.Email(r),
|
||||||
}
|
}
|
||||||
if err := t.Execute(w, data); err != nil {
|
if err := t.Execute(w, data); err != nil {
|
||||||
log.Printf("[ERROR] render directory page: %v", err)
|
log.Printf("[ERROR] render directory page: %v", err)
|
||||||
|
|||||||
@@ -9,11 +9,15 @@
|
|||||||
<link rel="stylesheet" href="/static/fonts/fonts.css">
|
<link rel="stylesheet" href="/static/fonts/fonts.css">
|
||||||
<link rel="stylesheet" href="/static/directory/style.css">
|
<link rel="stylesheet" href="/static/directory/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-user-email="{{.UserEmail}}">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand"><img src="/static/brand/icon-192.png" alt=""><span>Helios Who?</span></div>
|
<div class="brand"><img src="/static/brand/icon-192.png" alt=""><span>Helios Who?</span></div>
|
||||||
<nav id="nav"></nav>
|
<nav id="nav"></nav>
|
||||||
<div class="user"><span class="user-avatar">{{.UserInitial}}</span><span>{{.UserName}}</span></div>
|
<div class="user-menu" id="user-menu" hidden>
|
||||||
|
<a href="/profile">View Profile</a>
|
||||||
|
<form method="post" action="/auth/logout"><button>Sign Out</button></form>
|
||||||
|
</div>
|
||||||
|
<div class="user" id="user"><span class="user-avatar">{{.UserInitial}}</span><span>{{.UserName}}</span></div>
|
||||||
</aside>
|
</aside>
|
||||||
<main id="main"></main>
|
<main id="main"></main>
|
||||||
<script src="/static/directory/app.js"></script>
|
<script src="/static/directory/app.js"></script>
|
||||||
|
|||||||
@@ -359,6 +359,9 @@ function referrerCrumbs() {
|
|||||||
if (seg[0] === 'staff') {
|
if (seg[0] === 'staff') {
|
||||||
return [['Staff', '/staff']];
|
return [['Staff', '/staff']];
|
||||||
}
|
}
|
||||||
|
if (seg[0] === 'profile') {
|
||||||
|
return [['Profile', '/profile']];
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +586,17 @@ function renderFamilyDetail(key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const shortName = (family.name || '').replace(/ Family$/, '');
|
const shortName = (family.name || '').replace(/ Family$/, '');
|
||||||
main.append(breadcrumbs([['People', '/people'], [shortName, null], ['Family', null]]));
|
let crumbs = [['People', '/people'], [shortName, null], ['Family', null]];
|
||||||
|
if (document.referrer && new URL(document.referrer).origin === location.origin) {
|
||||||
|
const ref = new URL(document.referrer);
|
||||||
|
const rseg = ref.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
||||||
|
if (rseg[0] === 'people' && rseg[1] && byEmail[rseg[1]]) {
|
||||||
|
crumbs = [['People', '/people'], [byEmail[rseg[1]].fullName, ref.pathname], ['Family', null]];
|
||||||
|
} else if (rseg[0] === 'profile') {
|
||||||
|
crumbs = [['Profile', '/profile'], ['Family', null]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main.append(breadcrumbs(crumbs));
|
||||||
|
|
||||||
const content = el('div', 'container detail-content');
|
const content = el('div', 'container detail-content');
|
||||||
const grid = el('div', 'detail-grid');
|
const grid = el('div', 'detail-grid');
|
||||||
@@ -1046,6 +1059,30 @@ function renderClassroomDetail(slug) {
|
|||||||
renderRoster(classroom.name, classroom.imageUrl, groups);
|
renderRoster(classroom.name, classroom.imageUrl, groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderProfile() {
|
||||||
|
const main = document.querySelector('#main');
|
||||||
|
main.replaceChildren();
|
||||||
|
const me = byEmail[document.body.dataset.userEmail];
|
||||||
|
if (!me) {
|
||||||
|
main.append(el('div', 'empty', 'Not found.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = el('div', 'container content');
|
||||||
|
const family = state.model.families[me.familyKey];
|
||||||
|
if (family) {
|
||||||
|
content.append(el('h1', 'profile-heading', 'Family Photo'));
|
||||||
|
content.append(listRow(thumbUrl(family.photoUrl), '', 'Family Photo', family.photoCaption || '', '/families/' + encodeURIComponent(me.familyKey)));
|
||||||
|
const kids = (family.kidEmails || []).map(e => byEmail[e]).filter(Boolean);
|
||||||
|
if (kids.length) {
|
||||||
|
content.append(el('h1', 'profile-heading', 'Students'));
|
||||||
|
for (const k of kids) {
|
||||||
|
content.append(listRow(thumbUrl(k.photoUrl), (k.pronouns || '').toUpperCase(), firstName(k.fullName), '', personLink(k)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main.append(content);
|
||||||
|
}
|
||||||
|
|
||||||
function tabParam(fallback) {
|
function tabParam(fallback) {
|
||||||
return new URLSearchParams(location.search).get('tab') || fallback;
|
return new URLSearchParams(location.search).get('tab') || fallback;
|
||||||
}
|
}
|
||||||
@@ -1072,9 +1109,25 @@ function render() {
|
|||||||
} else if (seg[0] === 'staff') {
|
} else if (seg[0] === 'staff') {
|
||||||
state.q = '';
|
state.q = '';
|
||||||
renderStaffPage();
|
renderStaffPage();
|
||||||
|
} else if (seg[0] === 'profile') {
|
||||||
|
renderProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userMenu = document.querySelector('#user-menu');
|
||||||
|
document.querySelector('#user').addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
userMenu.hidden = !userMenu.hidden;
|
||||||
|
});
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
userMenu.hidden = true;
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
userMenu.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const res = await fetch('/api/directory/model');
|
const res = await fetch('/api/directory/model');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
@@ -101,6 +102,49 @@ nav a.active {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
bottom: 62px;
|
||||||
|
width: 190px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.25);
|
||||||
|
padding: 6px 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu a,
|
||||||
|
.user-menu button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu a:hover,
|
||||||
|
.user-menu button:hover {
|
||||||
|
background: #f2f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
@@ -202,6 +246,16 @@ main {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-heading {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 26px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-heading:first-child {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -427,6 +481,22 @@ a.family-card {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.person-card,
|
||||||
|
a.family-card {
|
||||||
|
padding: 14px;
|
||||||
|
margin: -14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.person-card:hover,
|
||||||
|
a.family-card:hover {
|
||||||
|
background: #f6f6f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.student-card:hover {
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.detail-top {
|
.detail-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -593,6 +663,17 @@ a.family-card {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.list-row {
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
|
margin: 0 -14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.list-row:hover {
|
||||||
|
background: #f6f6f7;
|
||||||
|
}
|
||||||
|
|
||||||
.list-tile {
|
.list-tile {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
|
|||||||
Reference in New Issue
Block a user