refactor(peer-support): delete provably dead code (Phase 1)

The Skills Exchange + Peer Support feature was replaced by Community
Connections on 2026-04-05, but several files and code paths were left
in place as backward-compat. None are reachable from the live UI:

- usePeerSupport.js composable: not imported anywhere
- PeerSupportBadge.vue: not imported anywhere
- peer-support.vue: stub redirect with no incoming links
- /api/peer-support.get.js: only consumed by usePeerSupport
- /api/members/me/peer-support.patch.js: same
- profile.patch.js offering/lookingFor write branches: profile form
  no longer sends these fields (only writes communityConnections.*)
- PEER_SUPPORT_ENABLED/DISABLED activity types and renderers: only
  written by the deleted peer-support.patch endpoint. The activityText
  formatter has a fallback for unknown types so existing records
  still display ("peer support enabled" with a generic icon).

Tests updated to drop peerSupportUpdateSchema coverage and the
offering/lookingFor passthrough assertion.

schemas.js cleanup deferred — concurrent communityConnections →
communityEcology rename is in flight in the working tree.
This commit is contained in:
Jennie Robinson Faber 2026-04-08 22:28:35 +01:00
parent 130e5bfa9f
commit 9577929e0d
11 changed files with 0 additions and 366 deletions

View file

@ -1,101 +0,0 @@
<template>
<!-- Corner Sticker Badge -->
<div
v-if="type === 'sticker'"
class="absolute top-2 right-2 z-10"
:title="title"
>
<div
class="relative transform rotate-3 hover:rotate-0 transition-transform"
style="width: 60px; height: 66px"
>
<!-- Shield background -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
class="absolute inset-0 w-full h-full drop-shadow-lg"
>
<path
d="M500 70 150 175.3v217.1C150 785 500 930 500 930s350-145 350-537.6V175.2L500 70Z"
class="fill-candlelight-500"
/>
</svg>
<!-- Content on top of shield -->
<div class="absolute inset-0 flex flex-col items-center justify-center">
<Icon
name="heroicons:chat-bubble-left-right-solid"
class="w-6 h-6 text-white"
/>
</div>
<!-- Sparkle effect -->
<div
class="absolute top-0 right-1 w-2 h-2 bg-candlelight-300 rounded-full animate-pulse"
></div>
</div>
</div>
<!-- Inline Badge -->
<div
v-else
:class="[
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-xs font-medium transition-all',
variant === 'default' &&
'bg-candlelight-900/20 text-candlelight-400 border-candlelight-500/40 hover:bg-candlelight-900/30',
variant === 'subtle' &&
'bg-candlelight-900/10 text-candlelight-500 border-candlelight-500/20',
variant === 'solid' &&
'bg-candlelight-500 text-white border-candlelight-600 hover:bg-candlelight-600',
]"
:title="title"
>
<Icon
name="heroicons:chat-bubble-left-right"
:class="[
'w-3.5 h-3.5',
variant === 'default' && 'text-candlelight-400',
variant === 'subtle' && 'text-candlelight-500',
variant === 'solid' && 'text-white',
]"
/>
<span>{{ label }}</span>
</div>
</template>
<script setup>
const props = defineProps({
/**
* Badge type - inline or corner sticker
* @values inline, sticker
*/
type: {
type: String,
default: "inline",
validator: (value) => ["inline", "sticker"].includes(value),
},
/**
* Display variant of the badge (for inline type)
* @values default, subtle, solid
*/
variant: {
type: String,
default: "default",
validator: (value) => ["default", "subtle", "solid"].includes(value),
},
/**
* Custom label text (defaults to "Offering Peer Support")
*/
label: {
type: String,
default: "Offering Peer Support",
},
/**
* Tooltip/title text
*/
title: {
type: String,
default: "This member offers 1:1 peer support sessions",
},
});
</script>

View file

@ -1,16 +0,0 @@
export const usePeerSupport = () => {
const updateSettings = async (settings) => {
return await $fetch('/api/members/me/peer-support', {
method: 'PATCH',
body: settings
});
};
const getSupporters = async (topic) => {
return await $fetch('/api/peer-support', {
query: topic ? { topic } : {}
});
};
return { updateSettings, getSupporters };
};

View file

@ -1,12 +0,0 @@
<template>
<div></div>
</template>
<script setup>
// Redirect to connections page
definePageMeta({
middleware: defineNuxtRouteMiddleware(() => {
return navigateTo("/connections");
}),
});
</script>

View file

@ -23,16 +23,6 @@ const formatters = {
link: null, link: null,
linkText: null linkText: null
}), }),
peer_support_enabled: (m) => ({
text: m.topics?.length
? `Enabled peer support (${m.topics.join(', ')})`
: 'Enabled peer support',
icon: 'i-lucide-users'
}),
peer_support_disabled: () => ({
text: 'Disabled peer support',
icon: 'i-lucide-users'
}),
circle_changed: (m) => ({ circle_changed: (m) => ({
text: `Changed circle from ${circleLabel(m.from)} to ${circleLabel(m.to)}`, text: `Changed circle from ${circleLabel(m.from)} to ${circleLabel(m.to)}`,
icon: 'i-lucide-refresh-cw' icon: 'i-lucide-refresh-cw'

View file

@ -1,97 +0,0 @@
import Member from '../../../models/member.js'
import { connectDB } from '../../../utils/mongoose.js'
export default defineEventHandler(async (event) => {
await connectDB()
const member = await requireAuth(event)
const body = await validateBody(event, peerSupportUpdateSchema)
// Build update object for peer support settings
const updateData = {
'peerSupport.enabled': body.enabled || false,
'peerSupport.skillTopics': body.skillTopics || [],
'peerSupport.supportTopics': body.supportTopics || [],
'peerSupport.availability': body.availability || '',
'peerSupport.personalMessage': body.personalMessage || '',
'peerSupport.slackUsername': body.slackUsername || '',
}
// If Slack username provided and peer support enabled, try to fetch Slack user ID
if (body.enabled && body.slackUsername) {
try {
console.log(
`[Peer Support] Attempting to fetch Slack user ID for: ${body.slackUsername}`,
)
const { getSlackService } = await import('../../../utils/slack.ts')
const slackService = getSlackService()
if (slackService) {
console.log('[Peer Support] Slack service initialized, looking up user...')
const slackUserId = await slackService.findUserIdByUsername(body.slackUsername)
if (slackUserId) {
updateData['slackUserId'] = slackUserId
console.log(
`[Peer Support] ✓ Found Slack user ID for ${body.slackUsername}: ${slackUserId}`,
)
console.log('[Peer Support] Opening DM channel...')
const dmChannelId = await slackService.openDMChannel(slackUserId)
if (dmChannelId) {
updateData['peerSupport.slackDMChannelId'] = dmChannelId
console.log(`[Peer Support] ✓ Got DM channel ID: ${dmChannelId}`)
} else {
console.warn('[Peer Support] Could not get DM channel ID')
}
} else {
console.warn(
`[Peer Support] Could not find Slack user ID for username: ${body.slackUsername}`,
)
}
} else {
console.log('[Peer Support] Slack service not configured, skipping user ID lookup')
}
} catch (error) {
console.error('[Peer Support] Error fetching Slack user ID:', error.message)
console.error('[Peer Support] Stack trace:', error.stack)
// Continue anyway - we'll still save the username
}
}
try {
const updated = await Member.findByIdAndUpdate(
member._id,
{ $set: updateData },
{ new: true, runValidators: true },
)
if (!updated) {
throw createError({
statusCode: 404,
statusMessage: 'Member not found',
})
}
if (body.enabled) {
logActivity(member._id, 'peer_support_enabled', {
topics: [...(body.skillTopics || []), ...(body.supportTopics || [])]
})
} else {
logActivity(member._id, 'peer_support_disabled', {})
}
return {
success: true,
peerSupport: updated.peerSupport,
}
} catch (error) {
console.error('Peer support update error:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to update peer support settings',
})
}
})

View file

@ -31,8 +31,6 @@ export default defineEventHandler(async (event) => {
"bioPrivacy", "bioPrivacy",
"locationPrivacy", "locationPrivacy",
"socialLinksPrivacy", "socialLinksPrivacy",
"offeringPrivacy",
"lookingForPrivacy",
"craftTagsPrivacy", "craftTagsPrivacy",
"communityConnectionsPrivacy", "communityConnectionsPrivacy",
]; ];
@ -51,20 +49,6 @@ export default defineEventHandler(async (event) => {
updateData.craftTags = body.craftTags; updateData.craftTags = body.craftTags;
} }
// Handle offering and lookingFor separately (nested objects)
if (body.offering !== undefined) {
updateData.offering = {
text: body.offering.text || "",
tags: body.offering.tags || [],
};
}
if (body.lookingFor !== undefined) {
updateData.lookingFor = {
text: body.lookingFor.text || "",
tags: body.lookingFor.tags || [],
};
}
// Handle privacy settings // Handle privacy settings
privacyFields.forEach((privacyField) => { privacyFields.forEach((privacyField) => {
if (body[privacyField] !== undefined) { if (body[privacyField] !== undefined) {
@ -107,8 +91,6 @@ export default defineEventHandler(async (event) => {
bio: member.bio, bio: member.bio,
location: member.location, location: member.location,
socialLinks: member.socialLinks, socialLinks: member.socialLinks,
offering: member.offering,
lookingFor: member.lookingFor,
craftTags: member.craftTags, craftTags: member.craftTags,
showInDirectory: member.showInDirectory, showInDirectory: member.showInDirectory,
notifications: member.notifications, notifications: member.notifications,

View file

@ -1,63 +0,0 @@
import jwt from "jsonwebtoken";
import Member from "../models/member.js";
import { connectDB } from "../utils/mongoose.js";
export default defineEventHandler(async (event) => {
await connectDB();
// Check if user is authenticated (optional for this endpoint)
const token = getCookie(event, "auth-token");
let isAuthenticated = false;
if (token) {
try {
jwt.verify(token, useRuntimeConfig().jwtSecret);
isAuthenticated = true;
} catch (err) {
isAuthenticated = false;
}
}
const query = getQuery(event);
const topic = query.topic;
// Build query for peer supporters
const dbQuery = {
"peerSupport.enabled": true,
status: "active",
};
// Filter by topic if specified
if (topic) {
dbQuery["peerSupport.topics"] = topic;
}
try {
const supporters = await Member.find(dbQuery)
.select(
"name avatar circle peerSupport slackUserId createdAt"
)
.sort({ createdAt: -1 })
.lean();
// Get unique topics for filter options
const allTopics = supporters
.flatMap((supporter) => supporter.peerSupport?.topics || [])
.filter((topic, index, self) => self.indexOf(topic) === index)
.sort();
return {
supporters,
totalCount: supporters.length,
filters: {
availableTopics: allTopics,
},
};
} catch (error) {
console.error("Peer support fetch error:", error);
throw createError({
statusCode: 500,
statusMessage: "Failed to fetch peer supporters",
});
}
});

View file

@ -5,8 +5,6 @@ const ACTIVITY_TYPES = [
'event_registered', 'event_registered',
'event_cancelled', 'event_cancelled',
'event_waitlisted', 'event_waitlisted',
'peer_support_enabled',
'peer_support_disabled',
'circle_changed', 'circle_changed',
'contribution_changed', 'contribution_changed',
'email_changed', 'email_changed',

View file

@ -5,8 +5,6 @@ export const ACTIVITY_TYPES = {
EVENT_REGISTERED: 'event_registered', EVENT_REGISTERED: 'event_registered',
EVENT_CANCELLED: 'event_cancelled', EVENT_CANCELLED: 'event_cancelled',
EVENT_WAITLISTED: 'event_waitlisted', EVENT_WAITLISTED: 'event_waitlisted',
PEER_SUPPORT_ENABLED: 'peer_support_enabled',
PEER_SUPPORT_DISABLED: 'peer_support_disabled',
CIRCLE_CHANGED: 'circle_changed', CIRCLE_CHANGED: 'circle_changed',
CONTRIBUTION_CHANGED: 'contribution_changed', CONTRIBUTION_CHANGED: 'contribution_changed',
EMAIL_CHANGED: 'email_changed', EMAIL_CHANGED: 'email_changed',
@ -29,8 +27,6 @@ export const ACTIVITY_TYPE_DEFAULTS = {
event_registered: 'public', event_registered: 'public',
event_cancelled: 'member', event_cancelled: 'member',
event_waitlisted: 'member', event_waitlisted: 'member',
peer_support_enabled: 'public',
peer_support_disabled: 'member',
circle_changed: 'member', circle_changed: 'member',
contribution_changed: 'member', contribution_changed: 'member',
email_changed: 'member', email_changed: 'member',

View file

@ -27,8 +27,6 @@ describe('members profile PATCH endpoint', () => {
bio: 'Updated bio', bio: 'Updated bio',
location: 'NYC', location: 'NYC',
socialLinks: { twitter: '@test' }, socialLinks: { twitter: '@test' },
offering: { text: 'help', tags: ['code'] },
lookingFor: { text: 'feedback', tags: ['design'] },
showInDirectory: true showInDirectory: true
} }
@ -137,21 +135,5 @@ describe('members profile PATCH endpoint', () => {
expect(setData).toHaveProperty('socialLinks') expect(setData).toHaveProperty('socialLinks')
}) })
it('passes offering and lookingFor nested objects through', async () => {
const event = createMockEvent({
method: 'PATCH',
path: '/api/members/profile',
body: {
offering: { text: 'mentoring', tags: ['code', 'design'] },
lookingFor: { text: 'feedback', tags: ['art'] }
}
})
await profilePatchHandler(event)
const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set
expect(setData.offering).toEqual({ text: 'mentoring', tags: ['code', 'design'] })
expect(setData.lookingFor).toEqual({ text: 'feedback', tags: ['art'] })
})
}) })
}) })

View file

@ -15,7 +15,6 @@ import {
guestRegisterSchema, guestRegisterSchema,
eventPaymentSchema, eventPaymentSchema,
updateContributionSchema, updateContributionSchema,
peerSupportUpdateSchema,
seriesTicketPurchaseSchema, seriesTicketPurchaseSchema,
seriesTicketEligibilitySchema, seriesTicketEligibilitySchema,
adminSeriesCreateSchema, adminSeriesCreateSchema,
@ -305,29 +304,6 @@ describe('updateContributionSchema', () => {
}) })
}) })
describe('peerSupportUpdateSchema', () => {
it('accepts valid peer support data', () => {
const result = peerSupportUpdateSchema.safeParse({
enabled: true,
skillTopics: ['game design', 'business'],
slackUsername: 'jane'
})
expect(result.success).toBe(true)
})
it('accepts empty object', () => {
const result = peerSupportUpdateSchema.safeParse({})
expect(result.success).toBe(true)
})
it('rejects non-array skillTopics', () => {
const result = peerSupportUpdateSchema.safeParse({
skillTopics: 'not-an-array'
})
expect(result.success).toBe(false)
})
})
// --- Series schemas --- // --- Series schemas ---
describe('seriesTicketPurchaseSchema', () => { describe('seriesTicketPurchaseSchema', () => {
@ -505,7 +481,6 @@ describe('validateBody migration coverage', () => {
'events/[id]/guest-register.post.js', 'events/[id]/guest-register.post.js',
'events/[id]/payment.post.js', 'events/[id]/payment.post.js',
'members/update-contribution.post.js', 'members/update-contribution.post.js',
'members/me/peer-support.patch.js',
'series/[id]/tickets/purchase.post.js', 'series/[id]/tickets/purchase.post.js',
'series/[id]/tickets/check-eligibility.post.js', 'series/[id]/tickets/check-eligibility.post.js',
'admin/series.post.js', 'admin/series.post.js',