refactor(community): rename Community Connections → Community Ecology
Some checks failed
Test / vitest (push) Successful in 11m42s
Test / playwright (push) Failing after 9m27s
Test / visual (push) Failing after 9m53s
Test / Notify on failure (push) Successful in 2s

Simplify the feature to pure discovery (filter by topic, see matching
members, copy Slack handle). Drop the connection request/confirm flow
entirely — Connection model, 7 API endpoints, useConnections composable,
and TagInput component deleted.

- Rename communityConnections → communityEcology in schema, API, pages
- Delete legacy fields: offering, lookingFor, peerSupport
- New /ecology page, /api/ecology/suggestions, community-ecology.patch
- Nav: "Connections" → "Ecology", remove pending-count badge
- Fix auth/member.get.js missing craftTags + communityEcology
- Add community_ecology_updated activity log type
- Expose slackHandle conditionally when offerPeerSupport is true
- Add migration script at scripts/migrate-to-ecology.js (run before deploy)
This commit is contained in:
Jennie Robinson Faber 2026-04-09 09:07:15 +01:00
parent 9577929e0d
commit 0b3896d984
33 changed files with 1002 additions and 2635 deletions

View file

@ -108,9 +108,9 @@
<div class="profile-bio" v-html="renderMarkdown(member.bio)"></div>
</div>
<!-- Two-column: Craft Tags + Community Connections -->
<!-- Two-column: Craft Tags + Community Ecology -->
<div
v-if="craftTagsDisplay.length > 0 || member.offering?.text || connectionTopicsDisplay.length > 0 || member.lookingFor?.text || member.communityConnections?.details"
v-if="craftTagsDisplay.length > 0 || ecologyTopics.length > 0 || member.communityEcology?.details"
class="profile-two-col"
>
<!-- Left: What I Do -->
@ -123,59 +123,39 @@
class="tag-pill"
>{{ tagLabel('craft', tag) }}</span>
</div>
<p v-if="member.offering?.text" class="profile-detail offering-text">
{{ member.offering.text }}
</p>
</div>
<!-- Right: Community Connections -->
<!-- Right: Community Ecology -->
<div class="profile-section">
<div class="section-label">Community Connections</div>
<div v-if="connectionTopicsDisplay.length > 0" class="tag-list">
<div class="section-label">Community Ecology</div>
<div v-if="ecologyTopics.length > 0" class="tag-list">
<span
v-for="topic in connectionTopicsDisplay"
:key="topic.tagSlug || topic"
v-for="topic in ecologyTopics"
:key="topic.tagSlug"
class="tag-pill connection-pill"
>
<span v-if="topic.state" class="connection-state">{{ stateLabel(topic.state) }}</span>
{{ tagLabel('cooperative', topic.tagSlug || topic) }}
{{ tagLabel('cooperative', topic.tagSlug) }}
</span>
</div>
<p v-if="member.communityConnections?.details" class="profile-detail connection-details">
{{ member.communityConnections.details }}
</p>
<p v-else-if="member.lookingFor?.text" class="profile-detail looking-text">
{{ member.lookingFor.text }}
<p v-if="member.communityEcology?.details" class="profile-detail connection-details">
{{ member.communityEcology.details }}
</p>
</div>
</div>
<!-- Peer Support -->
<div v-if="showPeerSupport" class="profile-section">
<div v-if="member.communityEcology?.offerPeerSupport" class="profile-section">
<div class="section-label">Peer Support</div>
<div class="dashed-box no-hover">
<div v-if="member.peerSupport?.skillTopics?.length" class="peer-group">
<span class="peer-label">Skills</span>
<div class="tag-list">
<span
v-for="topic in member.peerSupport.skillTopics"
:key="topic"
class="tag-pill"
>{{ topic }}</span>
</div>
</div>
<div v-if="member.peerSupport?.supportTopics?.length" class="peer-group">
<span class="peer-label">Topics</span>
<div class="tag-list">
<span
v-for="topic in member.peerSupport.supportTopics"
:key="topic"
class="tag-pill"
>{{ topic }}</span>
</div>
</div>
<p v-if="peerAvailability" class="profile-detail peer-availability">
{{ peerAvailability }}
<p v-if="member.communityEcology?.personalMessage" class="profile-detail">
{{ member.communityEcology.personalMessage }}
</p>
<p v-if="member.communityEcology?.availability" class="profile-detail peer-availability">
{{ member.communityEcology.availability }}
</p>
<p v-if="member.communityEcology?.slackHandle" class="profile-detail peer-availability">
Reach out on Slack: <span class="slack-handle">@{{ member.communityEcology.slackHandle }}</span>
</p>
</div>
</div>
@ -290,48 +270,11 @@ const tagLabel = (pool, slug) => {
return found ? found.label : slug;
};
// Craft tags display: new field, falling back to offering.tags
const craftTagsDisplay = computed(() => {
if (!member.value) return [];
if (member.value.craftTags && member.value.craftTags.length > 0) {
return member.value.craftTags;
}
return member.value.offering?.tags || [];
});
const craftTagsDisplay = computed(() => member.value?.craftTags || []);
// Connection topics display: new field, falling back to lookingFor.tags
const connectionTopicsDisplay = computed(() => {
if (!member.value) return [];
if (
member.value.communityConnections?.topics &&
member.value.communityConnections.topics.length > 0
) {
return member.value.communityConnections.topics;
}
if (member.value.lookingFor?.tags && member.value.lookingFor.tags.length > 0) {
return member.value.lookingFor.tags.map((tag) => ({ tagSlug: tag, state: null }));
}
return [];
});
// Peer support: check both new communityConnections and old peerSupport
const showPeerSupport = computed(() => {
if (!member.value) return false;
return (
member.value.communityConnections?.offerPeerSupport ||
member.value.peerSupport?.enabled
);
});
// Peer availability: prefer new field, fall back to old
const peerAvailability = computed(() => {
if (!member.value) return "";
return (
member.value.communityConnections?.availability ||
member.value.peerSupport?.availability ||
""
);
});
const ecologyTopics = computed(
() => member.value?.communityEcology?.topics || [],
);
// Whether the member has any social links (for hero layout)
const hasSocialLinks = computed(() =>
@ -587,8 +530,6 @@ useHead({
line-height: 1.6;
margin: 0;
}
.offering-text,
.looking-text,
.connection-details {
margin-top: 10px;
}
@ -623,26 +564,15 @@ useHead({
PEER SUPPORT
==================================================== */
.peer-group {
margin-bottom: 14px;
}
.peer-group:last-of-type {
margin-bottom: 0;
}
.peer-label {
font-family: "Commit Mono", monospace;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-faint);
display: block;
margin-bottom: 8px;
}
.peer-availability {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--border);
}
.slack-handle {
font-family: "Commit Mono", monospace;
color: var(--candle-dim);
}
/* ====================================================
ACTIVITY TIMELINE

View file

@ -183,32 +183,30 @@
}}
</div>
<!-- Craft tags (fall back to offering.tags) -->
<div
v-if="getMemberCraftTags(member).length > 0"
v-if="member.craftTags?.length > 0"
class="mc-tags"
>
<span class="tag-label">Craft:</span>
<span
v-for="tag in getMemberCraftTags(member)"
v-for="tag in member.craftTags"
:key="tag"
class="skill-tag"
>{{ craftTagLabel(tag) }}</span
>
</div>
<!-- Community connections topics (fall back to lookingFor.tags) -->
<div
v-if="getMemberConnectionTopics(member).length > 0"
v-if="member.communityEcology?.topics?.length > 0"
class="mc-looking"
>
<span
v-for="topic in getMemberConnectionTopics(member)"
:key="topic.tagSlug || topic"
v-for="topic in member.communityEcology.topics"
:key="topic.tagSlug"
class="connection-topic"
>
<span class="connection-state">{{ stateLabel(topic.state) }}</span>
{{ connectionTagLabel(topic.tagSlug || topic) }}
{{ connectionTagLabel(topic.tagSlug) }}
</span>
</div>
@ -319,36 +317,8 @@ const connectionTagLabel = (slug) => {
return found ? found.label : slug;
};
// Get craft tags for a member (new field, falling back to offering.tags)
const getMemberCraftTags = (member) => {
if (member.craftTags && member.craftTags.length > 0) {
return member.craftTags;
}
return member.offering?.tags || [];
};
// Get connection topics for a member (new field, falling back to lookingFor.tags)
const getMemberConnectionTopics = (member) => {
if (
member.communityConnections?.topics &&
member.communityConnections.topics.length > 0
) {
return member.communityConnections.topics;
}
// Fallback: wrap old lookingFor.tags as plain strings
if (member.lookingFor?.tags && member.lookingFor.tags.length > 0) {
return member.lookingFor.tags.map((tag) => ({ tagSlug: tag, state: null }));
}
return [];
};
// Show peer support link (check both old and new fields)
const showPeerSupport = (member) => {
if (member.communityConnections?.offerPeerSupport) return true;
if (member.peerSupport?.enabled && member.peerSupport?.slackUsername)
return true;
return false;
};
const showPeerSupport = (member) =>
!!member.communityEcology?.offerPeerSupport;
// Computed: has active filters
const hasActiveFilters = computed(() => {
@ -486,10 +456,7 @@ const clearAllFilters = () => {
// Slack DM functionality
const openSlackDM = async (member) => {
const username =
member.communityConnections?.slackHandle ||
member.peerSupport?.slackUsername ||
member.name;
const username = member.communityEcology?.slackHandle || member.name;
try {
await navigator.clipboard.writeText(username);