Skip to content

Commit d449534

Browse files
bokelleyclaude
andauthored
Add regional chapters and industry event presence features (#512)
* Add regional chapters and industry event presence features Enable user location tracking and event groups for member engagement: - Add user location tracking (city, country) with migration 105 - Add event groups (committee_type: 'event') linked to industry events with migration 106 - Implement Slack channel auto-sync: joining a channel auto-adds users to working groups - Add admin UI sections for event groups in Events admin page - Add 'event' committee type filter to Working Groups admin page - Create Addie tools for member-driven chapter creation - Add UUID validation and member count to event group API endpoints - Fix SQL injection in findChaptersNearLocation with proper LIKE escaping This enables temporary "chapter-like" groups for industry events like CES and Cannes Lions, where Slack channel membership automatically syncs to working group membership. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix migration version conflict with main branch Renumber migrations to avoid conflict with 106_system_settings.sql: - 106_user_location.sql → 107_user_location.sql - 107_event_groups.sql → 108_event_groups.sql 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent eb62450 commit d449534

File tree

12 files changed

+1441
-4
lines changed

12 files changed

+1441
-4
lines changed

.changeset/yummy-areas-bet.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
---
3+
4+
Add regional chapters and industry event presence features:
5+
- User location tracking (city, country) for chapter matching
6+
- Event groups (committee_type: 'event') linked to industry events
7+
- Slack channel auto-sync: join channel = join group
8+
- Admin UI for event groups and chapters
9+
- Addie tools for member-driven chapter creation

server/public/admin-events.html

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,48 @@
264264
font-weight: var(--font-semibold);
265265
color: var(--color-text-heading);
266266
}
267+
.event-group-section {
268+
margin-top: var(--space-4);
269+
padding: var(--space-4);
270+
background: var(--color-bg-subtle);
271+
border-radius: var(--radius-md);
272+
border: var(--border-1) solid var(--color-gray-200);
273+
}
274+
.event-group-section h4 {
275+
margin: 0 0 var(--space-2) 0;
276+
font-size: var(--text-sm);
277+
color: var(--color-text-heading);
278+
}
279+
.event-group-info {
280+
display: flex;
281+
align-items: center;
282+
gap: var(--space-3);
283+
flex-wrap: wrap;
284+
}
285+
.event-group-stat {
286+
font-size: var(--text-sm);
287+
color: var(--color-text-secondary);
288+
}
289+
.slack-link {
290+
display: inline-flex;
291+
align-items: center;
292+
gap: var(--space-1);
293+
color: var(--color-brand);
294+
text-decoration: none;
295+
font-size: var(--text-sm);
296+
}
297+
.slack-link:hover {
298+
text-decoration: underline;
299+
}
300+
.badge-event-group {
301+
display: inline-block;
302+
padding: 2px var(--space-2);
303+
border-radius: var(--radius-sm);
304+
font-size: var(--text-xs);
305+
font-weight: var(--font-medium);
306+
background: var(--color-success-100);
307+
color: var(--color-success-700);
308+
}
267309
</style>
268310
</head>
269311
<body>
@@ -527,6 +569,44 @@ <h2 id="modalTitle">Add Event</h2>
527569
</div>
528570
</div>
529571

572+
<!-- Attendee Group (only shown when editing existing event) -->
573+
<div id="eventGroupSection" style="display: none;">
574+
<div class="section-divider">
575+
<div class="section-title">Attendee Group</div>
576+
</div>
577+
<p style="font-size: var(--text-sm); color: var(--color-text-secondary); margin-bottom: var(--space-4);">
578+
Create a Slack channel for attendees to connect before, during, and after the event.
579+
Anyone who joins the channel is automatically added to the attendee group.
580+
</p>
581+
582+
<div id="eventGroupExists" style="display: none;" class="event-group-section">
583+
<h4>Attendee Group Active</h4>
584+
<div class="event-group-info">
585+
<span class="event-group-stat">
586+
<strong id="eventGroupMemberCount">0</strong> members
587+
</span>
588+
<a id="eventGroupSlackLink" href="#" target="_blank" class="slack-link">
589+
<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>
590+
<span id="eventGroupChannelName">#event-channel</span>
591+
</a>
592+
<button class="btn btn-secondary btn-small" onclick="viewEventGroupMembers()">View Members</button>
593+
</div>
594+
</div>
595+
596+
<div id="eventGroupNotExists" style="display: none;">
597+
<button type="button" class="btn btn-primary" onclick="createEventGroup()">
598+
Create Attendee Group + Slack Channel
599+
</button>
600+
<p style="font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--space-2);">
601+
This will create a public Slack channel for attendees to join.
602+
</p>
603+
</div>
604+
605+
<div id="eventGroupLoading" style="display: none; color: var(--color-text-secondary); font-size: var(--text-sm);">
606+
Loading attendee group info...
607+
</div>
608+
</div>
609+
530610
<div class="modal-buttons">
531611
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
532612
<button type="submit" class="btn btn-primary" id="saveBtn">Save Event</button>
@@ -641,6 +721,7 @@ <h3>${escapeHtml(event.title)}</h3>
641721
document.getElementById('eventForm').reset();
642722
document.getElementById('sponsorshipTiers').innerHTML = '';
643723
document.getElementById('stripeProductInfo').style.display = 'none';
724+
document.getElementById('eventGroupSection').style.display = 'none';
644725
sponsorshipTierCount = 0;
645726
toggleLocationFields();
646727
toggleSponsorshipTiers();
@@ -711,9 +792,99 @@ <h3>${escapeHtml(event.title)}</h3>
711792
}
712793

713794
toggleLocationFields();
795+
796+
// Show event group section for existing events
797+
document.getElementById('eventGroupSection').style.display = 'block';
798+
loadEventGroupInfo(id);
799+
714800
document.getElementById('eventModal').style.display = 'flex';
715801
}
716802

803+
// Load event group info
804+
async function loadEventGroupInfo(eventId) {
805+
const section = document.getElementById('eventGroupSection');
806+
const exists = document.getElementById('eventGroupExists');
807+
const notExists = document.getElementById('eventGroupNotExists');
808+
const loading = document.getElementById('eventGroupLoading');
809+
810+
// Show loading
811+
exists.style.display = 'none';
812+
notExists.style.display = 'none';
813+
loading.style.display = 'block';
814+
815+
try {
816+
const res = await fetch(`/api/admin/events/${eventId}/event-group`);
817+
if (!res.ok) throw new Error('Failed to load event group');
818+
819+
const data = await res.json();
820+
loading.style.display = 'none';
821+
822+
if (data.event_group) {
823+
// Event group exists
824+
exists.style.display = 'block';
825+
document.getElementById('eventGroupMemberCount').textContent = data.member_count || 0;
826+
827+
if (data.event_group.slack_channel_url) {
828+
document.getElementById('eventGroupSlackLink').href = data.event_group.slack_channel_url;
829+
document.getElementById('eventGroupChannelName').textContent =
830+
data.event_group.slack_channel_id ? `#${data.event_group.slug}` : 'Slack Channel';
831+
}
832+
} else {
833+
// No event group yet
834+
notExists.style.display = 'block';
835+
}
836+
} catch (error) {
837+
console.error('Error loading event group:', error);
838+
loading.style.display = 'none';
839+
notExists.style.display = 'block';
840+
}
841+
}
842+
843+
// Create event group
844+
async function createEventGroup() {
845+
if (!editingEventId) return;
846+
847+
const btn = document.querySelector('#eventGroupNotExists button');
848+
btn.disabled = true;
849+
btn.textContent = 'Creating...';
850+
851+
try {
852+
const res = await fetch(`/api/admin/events/${editingEventId}/event-group`, {
853+
method: 'POST',
854+
headers: { 'Content-Type': 'application/json' },
855+
body: JSON.stringify({ create_slack_channel: true })
856+
});
857+
858+
if (!res.ok) {
859+
const error = await res.json();
860+
throw new Error(error.message || 'Failed to create event group');
861+
}
862+
863+
const data = await res.json();
864+
865+
// Refresh the event group info
866+
await loadEventGroupInfo(editingEventId);
867+
868+
if (data.slack_channel_created) {
869+
alert('Attendee group and Slack channel created! Share the channel link with attendees.');
870+
} else {
871+
alert('Attendee group created. Note: Slack channel could not be created automatically - you may need to create it manually.');
872+
}
873+
} catch (error) {
874+
alert(error.message);
875+
btn.disabled = false;
876+
btn.textContent = 'Create Attendee Group + Slack Channel';
877+
}
878+
}
879+
880+
// View event group members
881+
function viewEventGroupMembers() {
882+
// Navigate to working groups admin filtered by this event group
883+
if (editingEventId) {
884+
window.open('/admin/working-groups?type=event', '_blank');
885+
}
886+
}
887+
717888
// Save event
718889
async function saveEvent(e) {
719890
e.preventDefault();
@@ -822,6 +993,11 @@ <h3>${escapeHtml(event.title)}</h3>
822993
function closeModal() {
823994
editingEventId = null;
824995
document.getElementById('eventModal').style.display = 'none';
996+
// Reset event group section
997+
document.getElementById('eventGroupSection').style.display = 'none';
998+
document.getElementById('eventGroupExists').style.display = 'none';
999+
document.getElementById('eventGroupNotExists').style.display = 'none';
1000+
document.getElementById('eventGroupLoading').style.display = 'none';
8251001
}
8261002

8271003
// Toggle location fields based on event format

server/public/admin-working-groups.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
.badge-council { background: var(--color-purple-100, #f3e8ff); color: var(--color-purple-700, #7c3aed); }
8181
.badge-chapter { background: var(--color-teal-100, #ccfbf1); color: var(--color-teal-700, #0f766e); }
8282
.badge-governance { background: var(--color-gray-300); color: var(--color-gray-700); }
83+
.badge-event { background: var(--color-warning-100); color: var(--color-warning-700); }
8384
.btn {
8485
padding: var(--space-2) var(--space-4);
8586
border: none;
@@ -348,6 +349,7 @@ <h1>Committees</h1>
348349
<option value="council">Industry Councils</option>
349350
<option value="chapter">Regional Chapters</option>
350351
<option value="governance">Governance</option>
352+
<option value="event">Event Groups</option>
351353
</select>
352354
<select id="statusFilter" onchange="renderGroupsList()">
353355
<option value="">All Statuses</option>
@@ -425,6 +427,7 @@ <h2 id="modalTitle">Add Committee</h2>
425427
<option value="council">Industry Council</option>
426428
<option value="chapter">Regional Chapter</option>
427429
<option value="governance">Governance</option>
430+
<option value="event">Event Group</option>
428431
</select>
429432
</div>
430433
</div>
@@ -682,7 +685,8 @@ <h2>Delete Committee</h2>
682685
working_group: 'Working Group',
683686
council: 'Industry Council',
684687
chapter: 'Regional Chapter',
685-
governance: 'Governance'
688+
governance: 'Governance',
689+
event: 'Event Group'
686690
};
687691

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

725732
html += `
726733
<div class="group-item">
@@ -731,6 +738,7 @@ <h3>${escapeHtml(group.name)}</h3>
731738
${statusBadge}
732739
${accessBadge}
733740
${regionDisplay}
741+
${eventDateDisplay}
734742
<span>${group.member_count || 0} members</span>
735743
${group.leaders && group.leaders.length > 0 ? `<span>· Leaders: ${group.leaders.map(l => escapeHtml(l.name || 'Unknown')).join(', ')}</span>` : ''}
736744
</div>

0 commit comments

Comments
 (0)