<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ project.name }} — sfpermits.ai</title>
{% include "fragments/head_obsidian.html" %}
<style nonce="{{ csp_nonce }}">
/* ── Project detail layout ────────────────────────────────── */
.project-detail-main {
padding: var(--space-8) 0 var(--space-16);
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-6);
}
/* Back link */
.back-link {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: var(--space-1);
transition: color 0.2s;
}
.back-link:hover { color: var(--text-primary); }
/* Project header */
.project-header h1 {
font-family: var(--sans);
font-size: var(--text-xl);
font-weight: 400;
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
}
.project-meta {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
}
/* Section card heading */
.section-card-heading {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-secondary);
margin: 0 0 var(--space-4) 0;
padding-bottom: var(--space-3);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
/* Member rows */
.member-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.member-row:last-child { border-bottom: none; }
.member-name {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-primary);
}
.member-email {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
margin-top: 2px;
}
/* Role badge */
.role-badge {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
}
.role-owner { background: rgba(34, 211, 238, 0.08); color: var(--signal-cyan); border: 1px solid rgba(34, 211, 238, 0.2); }
.role-admin { background: rgba(96, 165, 250, 0.08); color: var(--signal-blue); border: 1px solid rgba(96, 165, 250, 0.2); }
.role-member { background: rgba(255,255,255,0.04); color: var(--text-secondary); border: 1px solid rgba(255,255,255,0.06); }
/* Analysis rows */
.analysis-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.analysis-row:last-child { border-bottom: none; }
.analysis-row a {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--signal-cyan);
text-decoration: none;
transition: color 0.2s;
}
.analysis-row a:hover { text-decoration: underline; }
.analysis-meta {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
flex-shrink: 0;
}
/* Invite form */
.invite-section {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid rgba(255,255,255,0.06);
}
.invite-label {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: var(--space-3);
display: block;
}
.invite-form {
display: flex;
gap: var(--space-2);
}
.invite-form input[type="email"] {
flex: 1;
padding: 8px 12px;
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 300;
color: var(--text-primary);
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 6px;
outline: none;
transition: border-color 0.2s;
}
.invite-form input[type="email"]:focus {
border-color: rgba(34, 211, 238, 0.3);
}
.invite-form input[type="email"]::placeholder {
color: var(--text-tertiary);
}
/* Invite feedback */
.invite-feedback {
margin-top: var(--space-2);
font-family: var(--sans);
font-size: var(--text-xs);
}
.invite-feedback.ok { color: var(--signal-green); }
.invite-feedback.err { color: var(--signal-red); }
/* Empty list state */
.empty-list {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
padding: var(--space-3) 0;
}
</style>
</head>
<body class="obsidian">
{% include "fragments/nav.html" %}
<main>
<div class="obs-container project-detail-main">
<a href="/projects" class="back-link">← All Projects</a>
<div class="project-header">
<h1>{{ project.name }}</h1>
<div class="project-meta">
{% if project.address %}{{ project.address }}{% endif %}
{% if project.neighborhood %} · {{ project.neighborhood }}{% endif %}
{% if project.block and project.lot %} · Block {{ project.block }}, Lot {{ project.lot }}{% endif %}
</div>
</div>
<!-- Members section -->
<div class="glass-card" style="padding: var(--space-6);">
<h2 class="section-card-heading">Members <span style="color: var(--text-tertiary);">({{ members | length }})</span></h2>
{% if members %}
{% for m in members %}
<div class="member-row">
<div>
<div class="member-name">{{ m.display_name }}</div>
<div class="member-email">{{ m.email }}</div>
</div>
<span class="role-badge role-{{ m.role or 'member' }}">{{ (m.role or 'member') | capitalize }}</span>
</div>
{% endfor %}
{% else %}
<div class="empty-list">No members yet.</div>
{% endif %}
{% if my_role in ('owner', 'admin') or (g.user and g.user.is_admin) %}
<div class="invite-section">
<span class="invite-label">Invite a collaborator</span>
<div class="invite-form">
<input type="email" id="invite-email" placeholder="colleague@firm.com">
<button class="obsidian-btn obsidian-btn-primary" onclick="sendInvite()">Send Invite</button>
</div>
<div id="invite-feedback" class="invite-feedback" style="display:none;"></div>
</div>
{% endif %}
</div>
<!-- Analyses section -->
<div class="glass-card" style="padding: var(--space-6);">
<h2 class="section-card-heading">Analyses <span style="color: var(--text-tertiary);">({{ analyses | length }})</span></h2>
{% if analyses %}
{% for a in analyses %}
<div class="analysis-row">
<a href="/analysis/{{ a.id }}">{{ a.description or a.address or 'Untitled Analysis' }}</a>
<span class="analysis-meta">{{ a.created_at.strftime('%b %d, %Y') if a.created_at else '' }}</span>
</div>
{% endfor %}
{% else %}
<div class="empty-list">No analyses linked to this project yet.</div>
{% endif %}
</div>
</div>
</main>
<script nonce="{{ csp_nonce }}">
function sendInvite() {
const email = document.getElementById('invite-email').value.trim();
const feedback = document.getElementById('invite-feedback');
if (!email) { return; }
const projectId = {{ project.id | tojson }};
fetch('/project/' + projectId + '/invite', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.content || ''
},
body: JSON.stringify({email: email}),
})
.then(r => r.json())
.then(d => {
feedback.style.display = 'block';
if (d.ok) {
feedback.className = 'invite-feedback ok';
feedback.textContent = d.message || 'Invite sent!';
document.getElementById('invite-email').value = '';
} else {
feedback.className = 'invite-feedback err';
feedback.textContent = d.error || 'Could not send invite.';
}
})
.catch(() => {
feedback.style.display = 'block';
feedback.className = 'invite-feedback err';
feedback.textContent = 'Network error. Please try again.';
});
}
</script>
</body>
</html>