338 lines
12 KiB
Vue
338 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<PageHeader
|
|
title="Peer Support"
|
|
subtitle="Connect with fellow members for 1:1 guidance and support"
|
|
theme="purple"
|
|
size="medium"
|
|
/>
|
|
|
|
<section class="py-12 px-4">
|
|
<UContainer class="px-4">
|
|
<!-- Intro Text -->
|
|
<div
|
|
class="mb-8 backdrop-blur-sm bg-stone-900/50 border border-stone-700/50 rounded-lg p-6"
|
|
>
|
|
<p class="text-stone-300 mb-2">
|
|
Ghost Guild members offering peer support on various topics. Reach
|
|
out to schedule a conversation, ask questions, or get feedback on
|
|
your cooperative journey.
|
|
</p>
|
|
<p class="text-sm text-stone-400">
|
|
Interested in offering peer support?
|
|
<NuxtLink
|
|
to="/member/settings/peer-support"
|
|
class="text-purple-400 hover:text-purple-300 underline"
|
|
>
|
|
Enable it in your settings
|
|
</NuxtLink>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Topic Filter -->
|
|
<div v-if="availableTopics.length > 0" class="mb-8">
|
|
<div class="flex flex-wrap gap-2">
|
|
<span class="text-sm text-stone-400 mr-2 self-center"
|
|
>Filter by topic:</span
|
|
>
|
|
<button
|
|
type="button"
|
|
class="px-3 py-1 rounded-full text-sm transition-all border"
|
|
:class="
|
|
!selectedTopic
|
|
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
|
|
: 'bg-stone-800/50 text-stone-400 border-stone-700 hover:border-stone-600'
|
|
"
|
|
@click="
|
|
selectedTopic = null;
|
|
loadSupporters();
|
|
"
|
|
>
|
|
All Topics
|
|
</button>
|
|
<button
|
|
v-for="topic in availableTopics"
|
|
:key="topic"
|
|
type="button"
|
|
class="px-3 py-1 rounded-full text-sm transition-all border"
|
|
:class="
|
|
selectedTopic === topic
|
|
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
|
|
: 'bg-stone-800/50 text-stone-400 border-stone-700 hover:border-stone-600'
|
|
"
|
|
@click="
|
|
selectedTopic = topic;
|
|
loadSupporters();
|
|
"
|
|
>
|
|
{{ topic }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div
|
|
v-if="loading && !supporters.length"
|
|
class="flex justify-center items-center py-20"
|
|
>
|
|
<div class="text-center">
|
|
<div
|
|
class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
|
|
></div>
|
|
<p class="text-stone-400">Loading peer supporters...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Supporters Grid -->
|
|
<div v-else-if="supporters.length > 0">
|
|
<div class="mb-4 text-stone-400 text-sm">
|
|
{{ totalCount }}
|
|
{{ totalCount === 1 ? "peer supporter" : "peer supporters" }}
|
|
available
|
|
</div>
|
|
|
|
<div class="space-y-4 max-w-3xl">
|
|
<div
|
|
v-for="supporter in supporters"
|
|
:key="supporter._id"
|
|
class="backdrop-blur-sm bg-stone-900/50 border border-stone-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
|
|
>
|
|
<!-- Avatar and Name -->
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div
|
|
class="w-12 h-12 rounded-lg bg-stone-800 border border-stone-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
|
|
>
|
|
<img
|
|
v-if="supporter.avatar"
|
|
:src="`/ghosties/Ghost-${supporter.avatar.charAt(0).toUpperCase() + supporter.avatar.slice(1)}.png`"
|
|
:alt="supporter.name"
|
|
class="w-8 h-8 object-contain"
|
|
/>
|
|
<span v-else class="text-xl text-stone-600">👻</span>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="font-semibold text-stone-100 truncate">
|
|
{{ supporter.name }}
|
|
</h3>
|
|
<span
|
|
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30 inline-block"
|
|
>
|
|
{{ circleLabels[supporter.circle] }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Topics -->
|
|
<div
|
|
v-if="
|
|
supporter.peerSupport?.topics &&
|
|
supporter.peerSupport.topics.length > 0
|
|
"
|
|
class="mb-4"
|
|
>
|
|
<div class="text-xs text-stone-500 mb-2">Topics:</div>
|
|
<div class="flex flex-wrap gap-1">
|
|
<span
|
|
v-for="topic in supporter.peerSupport.topics"
|
|
:key="topic"
|
|
class="px-2 py-0.5 bg-stone-800/50 text-stone-300 rounded text-xs border border-stone-700"
|
|
>
|
|
{{ topic }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Availability -->
|
|
<div
|
|
v-if="supporter.peerSupport?.availability"
|
|
class="mb-4 text-sm text-stone-400"
|
|
>
|
|
<div class="text-xs text-stone-500 mb-1">Availability:</div>
|
|
{{ supporter.peerSupport.availability }}
|
|
</div>
|
|
|
|
<!-- Personal Message -->
|
|
<div
|
|
v-if="supporter.peerSupport?.personalMessage"
|
|
class="mb-4 text-sm text-stone-300 italic bg-stone-800/30 rounded-lg p-3 border-l-2 border-purple-500/50"
|
|
>
|
|
"{{ supporter.peerSupport.personalMessage }}"
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex gap-2 mt-4">
|
|
<a
|
|
v-if="supporter.peerSupport?.slackUsername"
|
|
:href="getSlackDMLink(supporter)"
|
|
@click.prevent="openSlackDM(supporter)"
|
|
class="flex-1 px-3 py-2 bg-purple-500/20 text-purple-300 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors text-center text-sm font-medium cursor-pointer"
|
|
>
|
|
Message {{ supporter.peerSupport.slackUsername }} on Slack
|
|
</a>
|
|
<a
|
|
v-else
|
|
href="slack://open"
|
|
@click.prevent="openSlackApp"
|
|
class="flex-1 px-3 py-2 bg-stone-800/50 text-stone-300 rounded border border-stone-700 hover:border-stone-600 transition-colors text-center text-sm font-medium cursor-pointer"
|
|
>
|
|
Find on Slack
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="text-center py-20">
|
|
<div class="w-16 h-16 mx-auto mb-4 opacity-50">
|
|
<svg
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
class="text-stone-600"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<h3 class="text-lg font-medium text-stone-300 mb-2">
|
|
No peer supporters yet
|
|
</h3>
|
|
<p class="text-stone-400 mb-6">
|
|
Be the first to offer peer support to the community!
|
|
</p>
|
|
<UButton to="/member/settings/peer-support">
|
|
Enable Peer Support
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- CTA for Members -->
|
|
<div
|
|
v-if="isAuthenticated && supporters.length > 0"
|
|
class="mt-8 backdrop-blur-sm bg-purple-500/10 border border-purple-500/30 rounded-lg p-6 text-center"
|
|
>
|
|
<p class="text-purple-200 mb-4">
|
|
💜 Want to offer peer support to fellow members?
|
|
</p>
|
|
<UButton to="/member/settings/peer-support" variant="outline">
|
|
Set Up Peer Support
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Not Authenticated Notice -->
|
|
<div
|
|
v-if="!isAuthenticated"
|
|
class="mt-8 backdrop-blur-sm bg-purple-500/10 border border-purple-500/30 rounded-lg p-6 text-center"
|
|
>
|
|
<p class="text-purple-200 mb-4">
|
|
🔒 Peer support is available to Ghost Guild members
|
|
</p>
|
|
<div class="flex gap-3 justify-center">
|
|
<UButton to="/login" variant="outline"> Log In </UButton>
|
|
<UButton to="/join"> Join Ghost Guild </UButton>
|
|
</div>
|
|
</div>
|
|
</UContainer>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { isAuthenticated } = useAuth();
|
|
const { getSupporters } = usePeerSupport();
|
|
|
|
// State
|
|
const supporters = ref([]);
|
|
const totalCount = ref(0);
|
|
const availableTopics = ref([]);
|
|
const loading = ref(false);
|
|
const selectedTopic = ref(null);
|
|
|
|
// Gamma Space Slack team ID
|
|
const slackTeamId = "T03A96LV4";
|
|
|
|
// Circle labels
|
|
const circleLabels = {
|
|
community: "Community",
|
|
founder: "Founder",
|
|
practitioner: "Practitioner",
|
|
};
|
|
|
|
// Get Slack DM deep link
|
|
const getSlackDMLink = (supporter) => {
|
|
// If we have the DM channel ID, use it for direct DM
|
|
if (supporter.peerSupport?.slackDMChannelId) {
|
|
return `slack://channel?team=${slackTeamId}&id=${supporter.peerSupport.slackDMChannelId}`;
|
|
}
|
|
// Otherwise fall back to opening workspace
|
|
return "slack://open";
|
|
};
|
|
|
|
// Open Slack DM
|
|
const openSlackDM = async (supporter) => {
|
|
console.log("Opening Slack DM for supporter:", supporter);
|
|
const username = supporter.peerSupport?.slackUsername || supporter.name;
|
|
|
|
// Copy username to clipboard
|
|
try {
|
|
await navigator.clipboard.writeText(username);
|
|
console.log("Copied username to clipboard:", username);
|
|
} catch (err) {
|
|
console.log("Could not copy to clipboard:", err);
|
|
}
|
|
|
|
// Show a toast/alert (you can replace this with a proper toast notification)
|
|
alert(
|
|
`Opening Slack...\n\nSearch for: ${username}\n\n(Username copied to clipboard)`,
|
|
);
|
|
|
|
// Open Slack workspace
|
|
window.open("https://gammaspace.slack.com", "_blank");
|
|
};
|
|
|
|
const openSlackApp = () => {
|
|
// Try to open Slack app
|
|
window.location.href = "slack://open";
|
|
|
|
// Fallback to web after a short delay if app doesn't open
|
|
setTimeout(() => {
|
|
window.open("https://gammaspace.slack.com", "_blank");
|
|
}, 1000);
|
|
};
|
|
|
|
// Load supporters
|
|
const loadSupporters = async () => {
|
|
loading.value = true;
|
|
|
|
try {
|
|
const data = await getSupporters(selectedTopic.value);
|
|
supporters.value = data.supporters;
|
|
totalCount.value = data.totalCount;
|
|
availableTopics.value = data.filters.availableTopics;
|
|
} catch (error) {
|
|
console.error("Failed to load peer supporters:", error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Load on mount
|
|
onMounted(() => {
|
|
loadSupporters();
|
|
});
|
|
|
|
useHead({
|
|
title: "Peer Support - Ghost Guild",
|
|
meta: [
|
|
{
|
|
name: "description",
|
|
content:
|
|
"Connect with Ghost Guild members for 1:1 guidance on governance, fundraising, team building, and more.",
|
|
},
|
|
],
|
|
});
|
|
</script>
|