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
9 changes: 9 additions & 0 deletions .changeset/yummy-areas-bet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
---

Add regional chapters and industry event presence features:
- User location tracking (city, country) for chapter matching
- Event groups (committee_type: 'event') linked to industry events
- Slack channel auto-sync: join channel = join group
- Admin UI for event groups and chapters
- Addie tools for member-driven chapter creation
176 changes: 176 additions & 0 deletions server/public/admin-events.html
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,48 @@
font-weight: var(--font-semibold);
color: var(--color-text-heading);
}
.event-group-section {
margin-top: var(--space-4);
padding: var(--space-4);
background: var(--color-bg-subtle);
border-radius: var(--radius-md);
border: var(--border-1) solid var(--color-gray-200);
}
.event-group-section h4 {
margin: 0 0 var(--space-2) 0;
font-size: var(--text-sm);
color: var(--color-text-heading);
}
.event-group-info {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.event-group-stat {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.slack-link {
display: inline-flex;
align-items: center;
gap: var(--space-1);
color: var(--color-brand);
text-decoration: none;
font-size: var(--text-sm);
}
.slack-link:hover {
text-decoration: underline;
}
.badge-event-group {
display: inline-block;
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-weight: var(--font-medium);
background: var(--color-success-100);
color: var(--color-success-700);
}
</style>
</head>
<body>
Expand Down Expand Up @@ -527,6 +569,44 @@ <h2 id="modalTitle">Add Event</h2>
</div>
</div>

<!-- Attendee Group (only shown when editing existing event) -->
<div id="eventGroupSection" style="display: none;">
<div class="section-divider">
<div class="section-title">Attendee Group</div>
</div>
<p style="font-size: var(--text-sm); color: var(--color-text-secondary); margin-bottom: var(--space-4);">
Create a Slack channel for attendees to connect before, during, and after the event.
Anyone who joins the channel is automatically added to the attendee group.
</p>

<div id="eventGroupExists" style="display: none;" class="event-group-section">
<h4>Attendee Group Active</h4>
<div class="event-group-info">
<span class="event-group-stat">
<strong id="eventGroupMemberCount">0</strong> members
</span>
<a id="eventGroupSlackLink" href="#" target="_blank" class="slack-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg>
<span id="eventGroupChannelName">#event-channel</span>
</a>
<button class="btn btn-secondary btn-small" onclick="viewEventGroupMembers()">View Members</button>
</div>
</div>

<div id="eventGroupNotExists" style="display: none;">
<button type="button" class="btn btn-primary" onclick="createEventGroup()">
Create Attendee Group + Slack Channel
</button>
<p style="font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--space-2);">
This will create a public Slack channel for attendees to join.
</p>
</div>

<div id="eventGroupLoading" style="display: none; color: var(--color-text-secondary); font-size: var(--text-sm);">
Loading attendee group info...
</div>
</div>

<div class="modal-buttons">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn btn-primary" id="saveBtn">Save Event</button>
Expand Down Expand Up @@ -641,6 +721,7 @@ <h3>${escapeHtml(event.title)}</h3>
document.getElementById('eventForm').reset();
document.getElementById('sponsorshipTiers').innerHTML = '';
document.getElementById('stripeProductInfo').style.display = 'none';
document.getElementById('eventGroupSection').style.display = 'none';
sponsorshipTierCount = 0;
toggleLocationFields();
toggleSponsorshipTiers();
Expand Down Expand Up @@ -711,9 +792,99 @@ <h3>${escapeHtml(event.title)}</h3>
}

toggleLocationFields();

// Show event group section for existing events
document.getElementById('eventGroupSection').style.display = 'block';
loadEventGroupInfo(id);

document.getElementById('eventModal').style.display = 'flex';
}

// Load event group info
async function loadEventGroupInfo(eventId) {
const section = document.getElementById('eventGroupSection');
const exists = document.getElementById('eventGroupExists');
const notExists = document.getElementById('eventGroupNotExists');
const loading = document.getElementById('eventGroupLoading');

// Show loading
exists.style.display = 'none';
notExists.style.display = 'none';
loading.style.display = 'block';

try {
const res = await fetch(`/api/admin/events/${eventId}/event-group`);
if (!res.ok) throw new Error('Failed to load event group');

const data = await res.json();
loading.style.display = 'none';

if (data.event_group) {
// Event group exists
exists.style.display = 'block';
document.getElementById('eventGroupMemberCount').textContent = data.member_count || 0;

if (data.event_group.slack_channel_url) {
document.getElementById('eventGroupSlackLink').href = data.event_group.slack_channel_url;
document.getElementById('eventGroupChannelName').textContent =
data.event_group.slack_channel_id ? `#${data.event_group.slug}` : 'Slack Channel';
}
} else {
// No event group yet
notExists.style.display = 'block';
}
} catch (error) {
console.error('Error loading event group:', error);
loading.style.display = 'none';
notExists.style.display = 'block';
}
}

// Create event group
async function createEventGroup() {
if (!editingEventId) return;

const btn = document.querySelector('#eventGroupNotExists button');
btn.disabled = true;
btn.textContent = 'Creating...';

try {
const res = await fetch(`/api/admin/events/${editingEventId}/event-group`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ create_slack_channel: true })
});

if (!res.ok) {
const error = await res.json();
throw new Error(error.message || 'Failed to create event group');
}

const data = await res.json();

// Refresh the event group info
await loadEventGroupInfo(editingEventId);

if (data.slack_channel_created) {
alert('Attendee group and Slack channel created! Share the channel link with attendees.');
} else {
alert('Attendee group created. Note: Slack channel could not be created automatically - you may need to create it manually.');
}
} catch (error) {
alert(error.message);
btn.disabled = false;
btn.textContent = 'Create Attendee Group + Slack Channel';
}
}

// View event group members
function viewEventGroupMembers() {
// Navigate to working groups admin filtered by this event group
if (editingEventId) {
window.open('/admin/working-groups?type=event', '_blank');
}
}

// Save event
async function saveEvent(e) {
e.preventDefault();
Expand Down Expand Up @@ -822,6 +993,11 @@ <h3>${escapeHtml(event.title)}</h3>
function closeModal() {
editingEventId = null;
document.getElementById('eventModal').style.display = 'none';
// Reset event group section
document.getElementById('eventGroupSection').style.display = 'none';
document.getElementById('eventGroupExists').style.display = 'none';
document.getElementById('eventGroupNotExists').style.display = 'none';
document.getElementById('eventGroupLoading').style.display = 'none';
}

// Toggle location fields based on event format
Expand Down
10 changes: 9 additions & 1 deletion server/public/admin-working-groups.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
.badge-council { background: var(--color-purple-100, #f3e8ff); color: var(--color-purple-700, #7c3aed); }
.badge-chapter { background: var(--color-teal-100, #ccfbf1); color: var(--color-teal-700, #0f766e); }
.badge-governance { background: var(--color-gray-300); color: var(--color-gray-700); }
.badge-event { background: var(--color-warning-100); color: var(--color-warning-700); }
.btn {
padding: var(--space-2) var(--space-4);
border: none;
Expand Down Expand Up @@ -348,6 +349,7 @@ <h1>Committees</h1>
<option value="council">Industry Councils</option>
<option value="chapter">Regional Chapters</option>
<option value="governance">Governance</option>
<option value="event">Event Groups</option>
</select>
<select id="statusFilter" onchange="renderGroupsList()">
<option value="">All Statuses</option>
Expand Down Expand Up @@ -425,6 +427,7 @@ <h2 id="modalTitle">Add Committee</h2>
<option value="council">Industry Council</option>
<option value="chapter">Regional Chapter</option>
<option value="governance">Governance</option>
<option value="event">Event Group</option>
</select>
</div>
</div>
Expand Down Expand Up @@ -682,7 +685,8 @@ <h2>Delete Committee</h2>
working_group: 'Working Group',
council: 'Industry Council',
chapter: 'Regional Chapter',
governance: 'Governance'
governance: 'Governance',
event: 'Event Group'
};

// Render groups list
Expand Down Expand Up @@ -721,6 +725,9 @@ <h2>Delete Committee</h2>
const regionDisplay = committeeType === 'chapter' && group.region
? `<span>📍 ${escapeHtml(group.region)}</span>`
: '';
const eventDateDisplay = committeeType === 'event' && group.event_start_date
? `<span>📅 ${new Date(group.event_start_date).toLocaleDateString()}</span>`
: '';

html += `
<div class="group-item">
Expand All @@ -731,6 +738,7 @@ <h3>${escapeHtml(group.name)}</h3>
${statusBadge}
${accessBadge}
${regionDisplay}
${eventDateDisplay}
<span>${group.member_count || 0} members</span>
${group.leaders && group.leaders.length > 0 ? `<span>· Leaders: ${group.leaders.map(l => escapeHtml(l.name || 'Unknown')).join(', ')}</span>` : ''}
</div>
Expand Down
Loading