feat: add craft tags and community connections to directory and profiles
Update member directory and public profile APIs to include craftTags and communityConnections with privacy-aware filtering. Directory now uses predefined tags from the Tag model for filter bars and supports craftTag/connectionTag query filters. Frontend shows craft tag pills and cooperative topics with state labels, falling back to old offering/lookingFor fields. Add Connections nav item.
This commit is contained in:
parent
bd07172093
commit
896de2e7fd
5 changed files with 367 additions and 138 deletions
|
|
@ -60,40 +60,42 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Offering Section -->
|
||||
<div
|
||||
v-if="member.offering?.tags?.length || member.offering?.text"
|
||||
class="profile-section"
|
||||
>
|
||||
<div class="section-label">Offering</div>
|
||||
<div v-if="member.offering.tags?.length" class="tag-list">
|
||||
<!-- What I Do (craft tags, falling back to offering) -->
|
||||
<div v-if="craftTagsDisplay.length > 0 || member.offering?.text" class="profile-section">
|
||||
<div class="section-label">What I Do</div>
|
||||
<div v-if="craftTagsDisplay.length > 0" class="tag-list">
|
||||
<span
|
||||
v-for="tag in member.offering.tags"
|
||||
v-for="tag in craftTagsDisplay"
|
||||
:key="tag"
|
||||
class="tag-pill"
|
||||
>{{ tag }}</span
|
||||
>{{ tagLabel('craft', tag) }}</span
|
||||
>
|
||||
</div>
|
||||
<p v-if="member.offering.text" class="profile-detail offering-text">
|
||||
<p v-if="member.offering?.text" class="profile-detail offering-text">
|
||||
{{ member.offering.text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Looking For Section -->
|
||||
<!-- Community Connections (cooperative topics with states, falling back to lookingFor) -->
|
||||
<div
|
||||
v-if="member.lookingFor?.tags?.length || member.lookingFor?.text"
|
||||
v-if="connectionTopicsDisplay.length > 0 || member.lookingFor?.text || member.communityConnections?.details"
|
||||
class="profile-section"
|
||||
>
|
||||
<div class="section-label">Looking for</div>
|
||||
<div v-if="member.lookingFor.tags?.length" class="tag-list">
|
||||
<div class="section-label">Community Connections</div>
|
||||
<div v-if="connectionTopicsDisplay.length > 0" class="tag-list">
|
||||
<span
|
||||
v-for="tag in member.lookingFor.tags"
|
||||
:key="tag"
|
||||
class="tag-pill"
|
||||
>{{ tag }}</span
|
||||
v-for="topic in connectionTopicsDisplay"
|
||||
:key="topic.tagSlug || topic"
|
||||
class="tag-pill connection-pill"
|
||||
>
|
||||
<span v-if="topic.state" class="connection-state">{{ stateLabel(topic.state) }}</span>
|
||||
{{ tagLabel('cooperative', topic.tagSlug || topic) }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="member.lookingFor.text" class="profile-detail looking-text">
|
||||
<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>
|
||||
</div>
|
||||
|
|
@ -150,10 +152,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peer Support Section -->
|
||||
<div v-if="member.peerSupport?.enabled" class="profile-section">
|
||||
<!-- Peer Support Section (reads from communityConnections, falls back to peerSupport) -->
|
||||
<div v-if="showPeerSupport" class="profile-section">
|
||||
<div class="section-label">Peer Support</div>
|
||||
<div v-if="member.peerSupport.skillTopics?.length" class="peer-group">
|
||||
<div v-if="member.peerSupport?.skillTopics?.length" class="peer-group">
|
||||
<span class="peer-label">Skills:</span>
|
||||
<div class="tag-list">
|
||||
<span
|
||||
|
|
@ -164,7 +166,7 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="member.peerSupport.supportTopics?.length" class="peer-group">
|
||||
<div v-if="member.peerSupport?.supportTopics?.length" class="peer-group">
|
||||
<span class="peer-label">Topics:</span>
|
||||
<div class="tag-list">
|
||||
<span
|
||||
|
|
@ -175,8 +177,8 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="member.peerSupport.availability" class="profile-detail">
|
||||
{{ member.peerSupport.availability }}
|
||||
<p v-if="peerAvailability" class="profile-detail">
|
||||
{{ peerAvailability }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -233,6 +235,15 @@ const circleLabels = {
|
|||
practitioner: "Practitioner",
|
||||
};
|
||||
|
||||
// State display text mapping
|
||||
const stateLabels = {
|
||||
help: "Can help",
|
||||
interested: "Interested",
|
||||
seeking: "Need help",
|
||||
};
|
||||
|
||||
const stateLabel = (state) => stateLabels[state] || state || "";
|
||||
|
||||
const getInitials = (name) => {
|
||||
if (!name) return "?";
|
||||
return name
|
||||
|
|
@ -246,6 +257,11 @@ const getInitials = (name) => {
|
|||
// Fetch member data — no await so the component renders immediately (no Suspense)
|
||||
const { data, pending, error: fetchError } = useFetch(`/api/members/${id}`);
|
||||
|
||||
// Fetch tags for slug-to-label lookup
|
||||
const { data: tagsData } = useFetch("/api/tags", {
|
||||
default: () => ({ tags: [] }),
|
||||
});
|
||||
|
||||
// Fetch public activity
|
||||
const { data: activityData } = useFetch(`/api/members/${id}/activity`, {
|
||||
params: { limit: 5 },
|
||||
|
|
@ -267,6 +283,56 @@ const formatRelativeDate = (date) => {
|
|||
}
|
||||
const member = computed(() => data.value?.member || null);
|
||||
|
||||
// Tag label lookup
|
||||
const tagLabel = (pool, slug) => {
|
||||
const tags = tagsData.value?.tags || [];
|
||||
const found = tags.find((t) => t.slug === slug && t.pool === pool);
|
||||
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 || [];
|
||||
});
|
||||
|
||||
// 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 pageBreadcrumbTitle = useState("pageBreadcrumbTitle", () => "");
|
||||
watch(
|
||||
member,
|
||||
|
|
@ -442,7 +508,8 @@ useHead({
|
|||
}
|
||||
|
||||
.offering-text,
|
||||
.looking-text {
|
||||
.looking-text,
|
||||
.connection-details {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +529,19 @@ useHead({
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.connection-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.connection-state {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* ---- SOCIAL LINKS ---- */
|
||||
.social-links {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue