From 1fc937a26a7a1ced6929de22c67f5f4ab4d6249f Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Tue, 14 Apr 2026 16:29:45 +0100 Subject: [PATCH] refactor(board): delete old board routes, absorb slackHandle into profile PATCH - Delete server/api/members/me/board.patch.js and server/api/board/suggestions.get.js - Add boardSlackHandle to memberProfileUpdateSchema; remove boardPrivacy - profile.patch.js: write boardSlackHandle -> board.slackHandle; drop boardPrivacy - Remove privacy.board field from Member model - onboarding/status.get.js: hasProfileTags now requires only craftTags; hasEngagedBoard uses BoardPost.exists - onboarding/track.post.js: graduation check uses BoardPost.exists instead of board.topics elemMatch - members/[id].get.js and directory.get.js: reduce board response to slackHandle only; drop connectionTag and peerSupport filters --- server/api/board/suggestions.get.js | 96 ---------------------------- server/api/members/[id].get.js | 18 +----- server/api/members/directory.get.js | 24 +------ server/api/members/me/board.patch.js | 70 -------------------- server/api/members/profile.patch.js | 6 +- server/api/onboarding/status.get.js | 10 ++- server/api/onboarding/track.post.js | 33 +++++----- server/models/member.js | 5 -- server/utils/schemas.js | 2 +- 9 files changed, 34 insertions(+), 230 deletions(-) delete mode 100644 server/api/board/suggestions.get.js delete mode 100644 server/api/members/me/board.patch.js diff --git a/server/api/board/suggestions.get.js b/server/api/board/suggestions.get.js deleted file mode 100644 index 103a521..0000000 --- a/server/api/board/suggestions.get.js +++ /dev/null @@ -1,96 +0,0 @@ -import Member from '../../models/member.js' -import { requireAuth } from '../../utils/auth.js' - -export default defineEventHandler(async (event) => { - const member = await requireAuth(event) - const memberId = member._id - - const topics = member.board?.topics || [] - if (!topics.length) { - return { suggestions: [] } - } - - const query = getQuery(event) - const filterTag = query.tag || null - - let myTopics = topics - if (filterTag) { - myTopics = myTopics.filter((t) => t.tagSlug === filterTag) - } - if (!myTopics.length) { - return { suggestions: [] } - } - - const mySlugs = myTopics.map((t) => t.tagSlug) - - const candidates = await Member.find({ - _id: { $ne: memberId }, - status: 'active', - 'board.topics.tagSlug': { $in: mySlugs }, - }) - .select('name avatar craftTags circle board privacy') - .lean() - - if (!candidates.length) { - return { suggestions: [] } - } - - const myTopicMap = {} - for (const t of myTopics) { - myTopicMap[t.tagSlug] = t.state - } - - const suggestions = [] - for (const candidate of candidates) { - const theirTopics = candidate.board?.topics || [] - const matchingTags = [] - - for (const theirTopic of theirTopics) { - const myState = myTopicMap[theirTopic.tagSlug] - if (!myState) continue - - matchingTags.push({ - tagSlug: theirTopic.tagSlug, - yourState: myState, - theirState: theirTopic.state, - }) - } - - if (!matchingTags.length) continue - - // Privacy filter: only expose fields the candidate allows to other members - const privacy = candidate.privacy || {} - const filtered = { - _id: candidate._id, - name: candidate.name, - circle: candidate.circle, - } - - const avatarPrivacy = privacy.avatar || 'public' - if (avatarPrivacy === 'public' || avatarPrivacy === 'members') { - filtered.avatar = candidate.avatar - } - - const craftTagsPrivacy = privacy.craftTags || 'members' - if (craftTagsPrivacy === 'public' || craftTagsPrivacy === 'members') { - filtered.craftTags = candidate.craftTags - } - - // Expose slackHandle only when the candidate has opted into peer support. - // Slack handle is the contact-in-place path — without it, there is no way - // for the current member to reach out. - if (candidate.board?.offerPeerSupport && candidate.board?.slackHandle) { - filtered.slackHandle = candidate.board.slackHandle - } - - suggestions.push({ - member: filtered, - matchingTags, - matchCount: matchingTags.length, - }) - } - - suggestions.sort((a, b) => b.matchCount - a.matchCount) - - return { suggestions } -}) diff --git a/server/api/members/[id].get.js b/server/api/members/[id].get.js index c5d910d..02546f2 100644 --- a/server/api/members/[id].get.js +++ b/server/api/members/[id].get.js @@ -70,21 +70,9 @@ export default defineEventHandler(async (event) => { if (isVisible("socialLinks")) filtered.socialLinks = member.socialLinks; if (isVisible("craftTags")) filtered.craftTags = member.craftTags; - if (isVisible("board")) { - const board = member.board || {}; - filtered.board = { - topics: board.topics, - offerPeerSupport: board.offerPeerSupport, - availability: board.availability, - details: board.details, - // Contact-in-place: surface the handle + personal message only when - // the member has explicitly opted into peer support. - ...(board.offerPeerSupport && { - slackHandle: board.slackHandle, - personalMessage: board.personalMessage, - }), - }; - } + filtered.board = { + slackHandle: member.board?.slackHandle, + }; return { member: filtered }; } catch (error) { diff --git a/server/api/members/directory.get.js b/server/api/members/directory.get.js index ecf8c3b..c0b7abc 100644 --- a/server/api/members/directory.get.js +++ b/server/api/members/directory.get.js @@ -22,9 +22,7 @@ export default defineEventHandler(async (event) => { const query = getQuery(event); const search = query.search || ""; const circle = query.circle || ""; - const peerSupport = query.peerSupport || ""; const craftTag = query.craftTag || ""; - const connectionTag = query.connectionTag || ""; const dbQuery = { showInDirectory: true, @@ -37,10 +35,6 @@ export default defineEventHandler(async (event) => { const andConditions = []; - if (peerSupport === "true") { - dbQuery["board.offerPeerSupport"] = true; - } - if (search) { const escaped = escapeRegex(search); andConditions.push({ @@ -55,10 +49,6 @@ export default defineEventHandler(async (event) => { dbQuery.craftTags = craftTag; } - if (connectionTag) { - dbQuery["board.topics.tagSlug"] = connectionTag; - } - if (andConditions.length > 0) { dbQuery.$and = andConditions; } @@ -96,17 +86,9 @@ export default defineEventHandler(async (event) => { if (isVisible("socialLinks")) filtered.socialLinks = member.socialLinks; if (isVisible("craftTags")) filtered.craftTags = member.craftTags; - if (isVisible("board")) { - const board = member.board || {}; - filtered.board = { - topics: board.topics, - offerPeerSupport: board.offerPeerSupport, - availability: board.availability, - ...(board.offerPeerSupport && { - slackHandle: board.slackHandle, - }), - }; - } + filtered.board = { + slackHandle: member.board?.slackHandle, + }; return filtered; }); diff --git a/server/api/members/me/board.patch.js b/server/api/members/me/board.patch.js deleted file mode 100644 index 3955429..0000000 --- a/server/api/members/me/board.patch.js +++ /dev/null @@ -1,70 +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, boardUpdateSchema) - - const updateData = { - 'board.topics': body.topics || [], - 'board.offerPeerSupport': body.offerPeerSupport || false, - 'board.availability': body.availability || '', - 'board.slackHandle': body.slackHandle || '', - 'board.personalMessage': body.personalMessage || '', - 'board.details': body.details || '', - } - - if (body.offerPeerSupport && body.slackHandle) { - try { - const { getSlackService } = await import('../../../utils/slack.ts') - const slackService = getSlackService() - - if (slackService) { - const slackUserId = await slackService.findUserIdByUsername(body.slackHandle) - if (slackUserId) { - updateData.slackUserId = slackUserId - } else { - console.warn( - `[Board] Could not find Slack user ID for handle: ${body.slackHandle}`, - ) - } - } - } catch (error) { - console.error('[Board] Error fetching Slack user ID:', error.message) - } - } - - try { - const updated = await Member.findByIdAndUpdate( - member._id, - { $set: updateData }, - { new: true, runValidators: true }, - ) - - if (!updated) { - throw createError({ - statusCode: 404, - statusMessage: 'Member not found', - }) - } - - logActivity(member._id, 'board_updated', { - topicCount: (body.topics || []).length, - offerPeerSupport: body.offerPeerSupport || false, - }) - - return { - success: true, - board: updated.board, - } - } catch (error) { - if (error.statusCode) throw error - console.error('Board update error:', error) - throw createError({ - statusCode: 500, - statusMessage: 'Failed to update board settings', - }) - } -}) diff --git a/server/api/members/profile.patch.js b/server/api/members/profile.patch.js index 7ce74d1..1cf266a 100644 --- a/server/api/members/profile.patch.js +++ b/server/api/members/profile.patch.js @@ -32,7 +32,6 @@ export default defineEventHandler(async (event) => { "locationPrivacy", "socialLinksPrivacy", "craftTagsPrivacy", - "boardPrivacy", ]; // Build update object from validated data @@ -49,6 +48,11 @@ export default defineEventHandler(async (event) => { updateData.craftTags = body.craftTags; } + // Handle board slack handle + if (body.boardSlackHandle !== undefined) { + updateData["board.slackHandle"] = body.boardSlackHandle; + } + // Handle privacy settings privacyFields.forEach((privacyField) => { if (body[privacyField] !== undefined) { diff --git a/server/api/onboarding/status.get.js b/server/api/onboarding/status.get.js index 33efffe..c1aa1fa 100644 --- a/server/api/onboarding/status.get.js +++ b/server/api/onboarding/status.get.js @@ -1,18 +1,16 @@ import { requireAuth } from '../../utils/auth.js' +import BoardPost from '../../models/boardPost.js' export default defineEventHandler(async (event) => { const member = await requireAuth(event) - const hasProfileTags = - member.craftTags.length > 0 && - (member.board?.topics || []).length > 0 + const hasProfileTags = member.craftTags.length > 0 const hasVisitedEvent = !!member.onboarding?.eventPageVisited - const topics = member.board?.topics || [] + const hasPosted = await BoardPost.exists({ author: member._id }) const hasEngagedBoard = - !!member.onboarding?.boardPageVisited && - topics.some((t) => ['help', 'interested', 'seeking'].includes(t.state)) + !!member.onboarding?.boardPageVisited && !!hasPosted const hasClickedWiki = !!member.onboarding?.wikiClicked diff --git a/server/api/onboarding/track.post.js b/server/api/onboarding/track.post.js index adbb7da..345d9f8 100644 --- a/server/api/onboarding/track.post.js +++ b/server/api/onboarding/track.post.js @@ -2,6 +2,7 @@ import { requireAuth } from '../../utils/auth.js' import { validateBody } from '../../utils/validateBody.js' import { onboardingTrackSchema } from '../../utils/schemas.js' import Member from '../../models/member.js' +import BoardPost from '../../models/boardPost.js' import { logActivity } from '../../utils/activityLog.js' export default defineEventHandler(async (event) => { @@ -26,22 +27,24 @@ export default defineEventHandler(async (event) => { // Log the individual goal completion await logActivity(member._id, 'member_onboarding_goal_completed', { goal }, { visibility: 'admin' }) + // Must have at least one board post to graduate + const hasPosted = await BoardPost.exists({ author: member._id }) + // Graduation check — atomic so concurrent requests can't double-graduate - const graduated = await Member.findOneAndUpdate( - { - _id: member._id, - 'onboarding.completedAt': null, - 'onboarding.eventPageVisited': true, - 'onboarding.boardPageVisited': true, - 'onboarding.wikiClicked': true, - 'craftTags.0': { $exists: true }, - 'board.topics': { - $elemMatch: { state: { $in: ['help', 'interested', 'seeking'] } }, - }, - }, - { $set: { 'onboarding.completedAt': new Date() } }, - { new: true } - ) + const graduated = hasPosted + ? await Member.findOneAndUpdate( + { + _id: member._id, + 'onboarding.completedAt': null, + 'onboarding.eventPageVisited': true, + 'onboarding.boardPageVisited': true, + 'onboarding.wikiClicked': true, + 'craftTags.0': { $exists: true }, + }, + { $set: { 'onboarding.completedAt': new Date() } }, + { new: true } + ) + : null if (graduated) { await logActivity(member._id, 'member_onboarding_completed', {}, { visibility: 'admin' }) diff --git a/server/models/member.js b/server/models/member.js index 9de5188..3dd5e3f 100644 --- a/server/models/member.js +++ b/server/models/member.js @@ -118,11 +118,6 @@ const memberSchema = new mongoose.Schema({ enum: ["public", "members", "private"], default: "members", }, - board: { - type: String, - enum: ["public", "members", "private"], - default: "members", - }, }, notifications: { diff --git a/server/utils/schemas.js b/server/utils/schemas.js index 018cae3..7fbd224 100644 --- a/server/utils/schemas.js +++ b/server/utils/schemas.js @@ -41,7 +41,7 @@ export const memberProfileUpdateSchema = z.object({ socialLinksPrivacy: privacyEnum.optional(), craftTags: z.array(z.string().max(100)).max(16).optional(), craftTagsPrivacy: privacyEnum.optional(), - boardPrivacy: privacyEnum.optional() + boardSlackHandle: z.string().max(200).optional() }) export const eventRegistrationSchema = z.object({