Merge feature/community-connections into main
Adds Community Connections system: predefined tags with engagement states, suggested connections page, and member discovery based on shared interests.
This commit is contained in:
commit
689548e389
33 changed files with 2743 additions and 407 deletions
|
|
@ -140,45 +140,16 @@
|
|||
</div>
|
||||
<PrivacyToggle v-model="formData.bioPrivacy" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills Exchange -->
|
||||
<hr class="section-divider" />
|
||||
<div class="profile-col-inset">
|
||||
<div class="section-label">Skills Exchange</div>
|
||||
|
||||
<!-- What I Do (craft tags) -->
|
||||
<div class="field">
|
||||
<label>What I Can Contribute</label>
|
||||
<TagInput
|
||||
v-model="formData.offering.tags"
|
||||
placeholder="add skill..."
|
||||
<label>What I Do</label>
|
||||
<CraftTagSelector
|
||||
v-model="formData.craftTags"
|
||||
:tags="craftTags"
|
||||
@suggest="tagSuggestPool = 'craft'; showTagSuggestModal = true"
|
||||
/>
|
||||
<PrivacyToggle v-model="formData.offeringPrivacy" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Details</label>
|
||||
<textarea
|
||||
v-model="formData.offering.text"
|
||||
rows="3"
|
||||
placeholder="e.g., I have 10+ years in Unity and love helping new devs."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>What I'm Looking For</label>
|
||||
<TagInput
|
||||
v-model="formData.lookingFor.tags"
|
||||
placeholder="add topic..."
|
||||
/>
|
||||
<PrivacyToggle v-model="formData.lookingForPrivacy" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Details</label>
|
||||
<textarea
|
||||
v-model="formData.lookingFor.text"
|
||||
rows="3"
|
||||
placeholder="e.g., Seeking a business-minded co-founder for a worker co-op studio."
|
||||
></textarea>
|
||||
<PrivacyToggle v-model="formData.craftTagsPrivacy" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -206,11 +177,34 @@
|
|||
<!-- ======== RIGHT COLUMN ======== -->
|
||||
<div class="profile-col-right">
|
||||
<div class="profile-col-inset">
|
||||
<div class="section-label">Peer Support</div>
|
||||
<div class="section-label">Community Connections</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Topics</label>
|
||||
<CooperativeTagSelector
|
||||
v-model="formData.communityConnectionsTopics"
|
||||
:tags="cooperativeTags"
|
||||
@suggest="tagSuggestPool = 'cooperative'; showTagSuggestModal = true"
|
||||
/>
|
||||
<PrivacyToggle v-model="formData.communityConnectionsPrivacy" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Details</label>
|
||||
<textarea
|
||||
v-model="formData.communityConnectionsDetails"
|
||||
rows="3"
|
||||
placeholder="What are you hoping to connect about?"
|
||||
maxlength="300"
|
||||
></textarea>
|
||||
<div class="char-count">
|
||||
{{ formData.communityConnectionsDetails?.length || 0 }} / 300
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-field">
|
||||
<USwitch
|
||||
v-model="formData.peerSupportEnabled"
|
||||
v-model="formData.communityConnectionsOfferPeerSupport"
|
||||
aria-label="Offer Peer Support"
|
||||
/>
|
||||
<div class="toggle-label">
|
||||
|
|
@ -221,47 +215,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.peerSupportEnabled" class="peer-panel">
|
||||
<div class="field">
|
||||
<label>Skill-Based Topics</label>
|
||||
<TagInput
|
||||
v-model="formData.peerSupportSkillTopics"
|
||||
placeholder="add topic..."
|
||||
/>
|
||||
<div v-if="suggestedSkillTopics.length" class="suggested">
|
||||
Suggested from your offerings:
|
||||
<a
|
||||
v-for="tag in suggestedSkillTopics"
|
||||
:key="tag"
|
||||
@click="addSuggestedSkillTopic(tag)"
|
||||
>{{ tag }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Conversational Topics</label>
|
||||
<div class="checkbox-grid">
|
||||
<label
|
||||
v-for="topic in availableSupportTopics"
|
||||
:key="topic"
|
||||
class="checkbox-item"
|
||||
:class="{
|
||||
checked:
|
||||
formData.peerSupportSupportTopics.includes(topic),
|
||||
}"
|
||||
@click.prevent="toggleSupportTopic(topic)"
|
||||
>
|
||||
<span class="cb">✓</span>
|
||||
{{ topic }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.communityConnectionsOfferPeerSupport" class="connections-panel">
|
||||
<div class="field">
|
||||
<label>Availability</label>
|
||||
<textarea
|
||||
v-model="formData.peerSupportAvailability"
|
||||
v-model="formData.communityConnectionsAvailability"
|
||||
rows="3"
|
||||
placeholder="e.g. Weekday afternoons ET"
|
||||
></textarea>
|
||||
|
|
@ -270,7 +228,7 @@
|
|||
<div class="field">
|
||||
<label>Slack Handle</label>
|
||||
<input
|
||||
v-model="formData.peerSupportSlackUsername"
|
||||
v-model="formData.communityConnectionsSlackHandle"
|
||||
type="text"
|
||||
placeholder="@yourslackname"
|
||||
/>
|
||||
|
|
@ -279,13 +237,13 @@
|
|||
<div class="field">
|
||||
<label>Personal Message</label>
|
||||
<textarea
|
||||
v-model="formData.peerSupportMessage"
|
||||
v-model="formData.communityConnectionsPersonalMessage"
|
||||
rows="3"
|
||||
maxlength="200"
|
||||
placeholder="Brief note shown to people requesting time with you"
|
||||
></textarea>
|
||||
<div class="char-count">
|
||||
{{ formData.peerSupportMessage?.length || 0 }} / 200
|
||||
{{ formData.communityConnectionsPersonalMessage?.length || 0 }} / 200
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -324,11 +282,11 @@
|
|||
|
||||
<div class="toggle-field">
|
||||
<USwitch
|
||||
v-model="formData.notifyPeerRequests"
|
||||
aria-label="Peer support requests"
|
||||
v-model="formData.notifyConnectionRequests"
|
||||
aria-label="Connection requests"
|
||||
/>
|
||||
<div class="toggle-label">
|
||||
Peer support requests
|
||||
Connection requests
|
||||
<span class="toggle-sub"
|
||||
>When someone wants to connect</span
|
||||
>
|
||||
|
|
@ -360,6 +318,9 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tag Suggest Modal -->
|
||||
<TagSuggestModal v-model:open="showTagSuggestModal" :pool="tagSuggestPool" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -389,6 +350,20 @@ const availableGhosts = [
|
|||
{ value: "wtf", label: "WTF", image: "/ghosties/Ghost-WTF.png" },
|
||||
];
|
||||
|
||||
// Fetch tags and split into pools
|
||||
const { data: tagsData } = await useFetch("/api/tags");
|
||||
|
||||
const craftTags = computed(() =>
|
||||
(tagsData.value?.tags || []).filter((t) => t.pool === "craft"),
|
||||
);
|
||||
const cooperativeTags = computed(() =>
|
||||
(tagsData.value?.tags || []).filter((t) => t.pool === "cooperative"),
|
||||
);
|
||||
|
||||
// Tag suggest modal state
|
||||
const showTagSuggestModal = ref(false);
|
||||
const tagSuggestPool = ref("");
|
||||
|
||||
// Form state
|
||||
const formData = reactive({
|
||||
name: "",
|
||||
|
|
@ -398,16 +373,18 @@ const formData = reactive({
|
|||
studio: "",
|
||||
bio: "",
|
||||
location: "",
|
||||
offering: { text: "", tags: [] },
|
||||
lookingFor: { text: "", tags: [] },
|
||||
showInDirectory: true,
|
||||
// Peer support
|
||||
peerSupportEnabled: false,
|
||||
peerSupportSkillTopics: [],
|
||||
peerSupportSupportTopics: [],
|
||||
peerSupportAvailability: "",
|
||||
peerSupportMessage: "",
|
||||
peerSupportSlackUsername: "",
|
||||
// Craft tags
|
||||
craftTags: [],
|
||||
craftTagsPrivacy: "members",
|
||||
// Community connections
|
||||
communityConnectionsTopics: [],
|
||||
communityConnectionsPrivacy: "members",
|
||||
communityConnectionsDetails: "",
|
||||
communityConnectionsOfferPeerSupport: false,
|
||||
communityConnectionsAvailability: "",
|
||||
communityConnectionsSlackHandle: "",
|
||||
communityConnectionsPersonalMessage: "",
|
||||
// Privacy
|
||||
pronounsPrivacy: "members",
|
||||
timeZonePrivacy: "members",
|
||||
|
|
@ -415,12 +392,10 @@ const formData = reactive({
|
|||
studioPrivacy: "members",
|
||||
bioPrivacy: "members",
|
||||
locationPrivacy: "members",
|
||||
offeringPrivacy: "members",
|
||||
lookingForPrivacy: "members",
|
||||
// Notifications
|
||||
notifyEvents: true,
|
||||
notifyUpdates: true,
|
||||
notifyPeerRequests: true,
|
||||
notifyConnectionRequests: true,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
|
@ -429,47 +404,11 @@ const saveSuccess = ref(false);
|
|||
const saveError = ref(null);
|
||||
const initialData = ref(null);
|
||||
|
||||
// Available conversational support topics
|
||||
const availableSupportTopics = [
|
||||
"Co-founder relationships",
|
||||
"Burnout prevention",
|
||||
"Impostor syndrome",
|
||||
"Work-life boundaries",
|
||||
"Conflict resolution",
|
||||
"General chat & support",
|
||||
];
|
||||
|
||||
// Computed
|
||||
const hasChanges = computed(() => {
|
||||
return JSON.stringify(formData) !== JSON.stringify(initialData.value);
|
||||
});
|
||||
|
||||
const suggestedSkillTopics = computed(() => {
|
||||
if (!formData.offering.tags?.length) return [];
|
||||
return formData.offering.tags.filter(
|
||||
(t) => !formData.peerSupportSkillTopics?.includes(t),
|
||||
);
|
||||
});
|
||||
|
||||
// Toggle a support topic in/out of the selection
|
||||
const toggleSupportTopic = (topic) => {
|
||||
const idx = formData.peerSupportSupportTopics.indexOf(topic);
|
||||
if (idx >= 0) {
|
||||
formData.peerSupportSupportTopics.splice(idx, 1);
|
||||
} else {
|
||||
formData.peerSupportSupportTopics.push(topic);
|
||||
}
|
||||
};
|
||||
|
||||
const addSuggestedSkillTopic = (tag) => {
|
||||
if (!Array.isArray(formData.peerSupportSkillTopics)) {
|
||||
formData.peerSupportSkillTopics = [];
|
||||
}
|
||||
if (!formData.peerSupportSkillTopics.includes(tag)) {
|
||||
formData.peerSupportSkillTopics.push(tag);
|
||||
}
|
||||
};
|
||||
|
||||
// Load member data into form
|
||||
const loadProfile = () => {
|
||||
if (memberData.value) {
|
||||
|
|
@ -481,66 +420,21 @@ const loadProfile = () => {
|
|||
formData.bio = memberData.value.bio || "";
|
||||
formData.location = memberData.value.location || "";
|
||||
|
||||
// Load offering (handle both old string and new object format)
|
||||
if (typeof memberData.value.offering === "string") {
|
||||
formData.offering.text = memberData.value.offering;
|
||||
formData.offering.tags = [];
|
||||
} else if (memberData.value.offering) {
|
||||
formData.offering.text = memberData.value.offering?.text || "";
|
||||
formData.offering.tags = Array.isArray(memberData.value.offering?.tags)
|
||||
? [...memberData.value.offering.tags]
|
||||
: [];
|
||||
} else {
|
||||
formData.offering.text = "";
|
||||
formData.offering.tags = [];
|
||||
}
|
||||
|
||||
// Load lookingFor (handle both old string and new object format)
|
||||
if (typeof memberData.value.lookingFor === "string") {
|
||||
formData.lookingFor.text = memberData.value.lookingFor;
|
||||
formData.lookingFor.tags = [];
|
||||
} else if (memberData.value.lookingFor) {
|
||||
formData.lookingFor.text = memberData.value.lookingFor?.text || "";
|
||||
formData.lookingFor.tags = Array.isArray(
|
||||
memberData.value.lookingFor?.tags,
|
||||
)
|
||||
? [...memberData.value.lookingFor.tags]
|
||||
: [];
|
||||
} else {
|
||||
formData.lookingFor.text = "";
|
||||
formData.lookingFor.tags = [];
|
||||
}
|
||||
|
||||
formData.showInDirectory = memberData.value.showInDirectory ?? true;
|
||||
|
||||
// Load peer support data
|
||||
if (memberData.value.peerSupport) {
|
||||
formData.peerSupportEnabled =
|
||||
memberData.value.peerSupport.enabled || false;
|
||||
formData.peerSupportSkillTopics = Array.isArray(
|
||||
memberData.value.peerSupport.skillTopics,
|
||||
)
|
||||
? [...memberData.value.peerSupport.skillTopics]
|
||||
: [];
|
||||
formData.peerSupportSupportTopics = Array.isArray(
|
||||
memberData.value.peerSupport.supportTopics,
|
||||
)
|
||||
? [...memberData.value.peerSupport.supportTopics]
|
||||
: [];
|
||||
formData.peerSupportAvailability =
|
||||
memberData.value.peerSupport.availability || "";
|
||||
formData.peerSupportMessage =
|
||||
memberData.value.peerSupport.personalMessage || "";
|
||||
formData.peerSupportSlackUsername =
|
||||
memberData.value.peerSupport.slackUsername || "";
|
||||
} else {
|
||||
formData.peerSupportEnabled = false;
|
||||
formData.peerSupportSkillTopics = [];
|
||||
formData.peerSupportSupportTopics = [];
|
||||
formData.peerSupportAvailability = "";
|
||||
formData.peerSupportMessage = "";
|
||||
formData.peerSupportSlackUsername = "";
|
||||
}
|
||||
// Load craft tags
|
||||
formData.craftTags = Array.isArray(memberData.value.craftTags)
|
||||
? [...memberData.value.craftTags]
|
||||
: [];
|
||||
|
||||
// Load community connections (with fallback to old peerSupport fields)
|
||||
const cc = memberData.value.communityConnections || {};
|
||||
formData.communityConnectionsTopics = Array.isArray(cc.topics) ? [...cc.topics] : [];
|
||||
formData.communityConnectionsOfferPeerSupport = cc.offerPeerSupport ?? memberData.value.peerSupport?.enabled ?? false;
|
||||
formData.communityConnectionsAvailability = cc.availability || memberData.value.peerSupport?.availability || "";
|
||||
formData.communityConnectionsSlackHandle = cc.slackHandle || memberData.value.peerSupport?.slackUsername || "";
|
||||
formData.communityConnectionsPersonalMessage = cc.personalMessage || memberData.value.peerSupport?.personalMessage || "";
|
||||
formData.communityConnectionsDetails = cc.details || "";
|
||||
|
||||
// Load privacy settings (with defaults)
|
||||
const privacy = memberData.value.privacy || {};
|
||||
|
|
@ -550,14 +444,14 @@ const loadProfile = () => {
|
|||
formData.studioPrivacy = privacy.studio || "members";
|
||||
formData.bioPrivacy = privacy.bio || "members";
|
||||
formData.locationPrivacy = privacy.location || "members";
|
||||
formData.offeringPrivacy = privacy.offering || "members";
|
||||
formData.lookingForPrivacy = privacy.lookingFor || "members";
|
||||
formData.craftTagsPrivacy = privacy.craftTags || "members";
|
||||
formData.communityConnectionsPrivacy = privacy.communityConnections || "members";
|
||||
|
||||
// Load notification prefs
|
||||
const notifs = memberData.value.notifications || {};
|
||||
formData.notifyEvents = notifs.events ?? true;
|
||||
formData.notifyUpdates = notifs.updates ?? true;
|
||||
formData.notifyPeerRequests = notifs.peerRequests ?? true;
|
||||
formData.notifyConnectionRequests = notifs.connectionRequests ?? notifs.peerRequests ?? true;
|
||||
|
||||
// Store initial state for change detection
|
||||
initialData.value = JSON.parse(JSON.stringify(formData));
|
||||
|
|
@ -571,29 +465,30 @@ const handleSubmit = async () => {
|
|||
saveError.value = null;
|
||||
|
||||
try {
|
||||
// Save profile data
|
||||
// Save profile data (includes craft tags + privacy + notifications)
|
||||
await $fetch("/api/members/profile", {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
...formData,
|
||||
craftTags: formData.craftTags,
|
||||
notifications: {
|
||||
events: formData.notifyEvents,
|
||||
updates: formData.notifyUpdates,
|
||||
peerRequests: formData.notifyPeerRequests,
|
||||
connectionRequests: formData.notifyConnectionRequests,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Save peer support data separately
|
||||
await $fetch("/api/members/me/peer-support", {
|
||||
// Save community connections data
|
||||
await $fetch("/api/members/me/community-connections", {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
enabled: formData.peerSupportEnabled,
|
||||
skillTopics: formData.peerSupportSkillTopics,
|
||||
supportTopics: formData.peerSupportSupportTopics,
|
||||
availability: formData.peerSupportAvailability,
|
||||
personalMessage: formData.peerSupportMessage,
|
||||
slackUsername: formData.peerSupportSlackUsername,
|
||||
topics: formData.communityConnectionsTopics,
|
||||
offerPeerSupport: formData.communityConnectionsOfferPeerSupport,
|
||||
availability: formData.communityConnectionsAvailability,
|
||||
slackHandle: formData.communityConnectionsSlackHandle,
|
||||
personalMessage: formData.communityConnectionsPersonalMessage,
|
||||
details: formData.communityConnectionsDetails,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -844,48 +739,8 @@ useHead({
|
|||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* ---- CHECKBOX GRID ---- */
|
||||
.checkbox-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3px 12px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-item .cb {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border: 1px dashed var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
color: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-item.checked .cb {
|
||||
border-color: var(--candle);
|
||||
border-style: solid;
|
||||
color: var(--candle);
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---- PEER SUPPORT PANEL ---- */
|
||||
.peer-panel {
|
||||
/* ---- CONNECTIONS PANEL ---- */
|
||||
.connections-panel {
|
||||
border: 1px dashed var(--border);
|
||||
padding: 12px 14px;
|
||||
margin-top: 4px;
|
||||
|
|
@ -893,19 +748,6 @@ useHead({
|
|||
background: var(--surface);
|
||||
}
|
||||
|
||||
.peer-panel .suggested {
|
||||
font-size: 10px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.peer-panel .suggested a {
|
||||
color: var(--candle);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* ---- DISABLED BUTTON ---- */
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
|
|
@ -962,10 +804,6 @@ useHead({
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.checkbox-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.profile-col-left .profile-col-inset,
|
||||
.profile-col-right .profile-col-inset {
|
||||
padding-left: 16px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue