Add Markdown support and update member features

The commit adds Markdown rendering capabilities and makes several UI/UX
improvements across member-related features including profile display,
peer support badges, and navigation structure.

Includes:
- Added @tailwindcss/typography plugin
- New Markdown rendering composable
- Simplified member navigation links
- Enhanced member profile layout and styling
- Added peer support badge component
- Improved mobile responsiveness
- Removed redundant icons and simplified UI
This commit is contained in:
Jennie Robinson Faber 2025-10-07 15:07:27 +01:00
parent fb02688166
commit 1f7a0f40c0
11 changed files with 375 additions and 432 deletions

View file

@ -2,6 +2,8 @@
@import "tailwindcss"; @import "tailwindcss";
@import "@nuxt/ui"; @import "@nuxt/ui";
@plugin "@tailwindcss/typography";
@theme { @theme {
/* Font families */ /* Font families */
--font-sans: "Inter", sans-serif; --font-sans: "Inter", sans-serif;

View file

@ -157,7 +157,6 @@ const memberNavigationItems = [
{ label: "Events", path: "/events" }, { label: "Events", path: "/events" },
{ label: "Members", path: "/members" }, { label: "Members", path: "/members" },
{ label: "Resources", path: "/resources" }, { label: "Resources", path: "/resources" },
{ label: "Peer Support", path: "/peer-support" },
{ label: "Profile", path: "/member/profile" }, { label: "Profile", path: "/member/profile" },
]; ];

View file

@ -0,0 +1,87 @@
<template>
<!-- Corner Sticker Badge -->
<div
v-if="type === 'sticker'"
class="absolute top-2 right-2 z-10"
:title="title"
>
<div
class="relative bg-gradient-to-br from-purple-500 to-purple-600 text-white px-3 py-2 rounded-lg shadow-lg border-2 border-purple-400/50 transform rotate-3 hover:rotate-0 transition-transform"
>
<div class="flex flex-col items-center gap-0.5">
<Icon name="heroicons:chat-bubble-left-right-solid" class="w-4 h-4" />
<span
class="text-[10px] font-bold uppercase tracking-wide leading-tight"
>Peer<br />Support</span
>
</div>
<!-- Sparkle effect -->
<div
class="absolute -top-1 -right-1 w-2 h-2 bg-yellow-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-purple-500/20 text-purple-300 border-purple-500/40 hover:bg-purple-500/30',
variant === 'subtle' &&
'bg-purple-500/10 text-purple-400 border-purple-500/20',
variant === 'solid' &&
'bg-purple-500 text-white border-purple-600 hover:bg-purple-600',
]"
:title="title"
>
<Icon
name="heroicons:chat-bubble-left-right"
:class="[
'w-3.5 h-3.5',
variant === 'default' && 'text-purple-300',
variant === 'subtle' && 'text-purple-400',
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

@ -0,0 +1,15 @@
import { marked } from 'marked'
export const useMarkdown = () => {
const render = (markdown) => {
if (!markdown) return ''
return marked(markdown, {
breaks: true,
gfm: true
})
}
return {
render
}
}

View file

@ -16,10 +16,6 @@
class="min-h-screen bg-ghost-900 flex items-center justify-center" class="min-h-screen bg-ghost-900 flex items-center justify-center"
> >
<div class="text-center"> <div class="text-center">
<Icon
name="heroicons:exclamation-triangle"
class="w-16 h-16 text-red-500 mx-auto mb-4"
/>
<h2 class="text-2xl font-bold text-ghost-100 mb-2">Event Not Found</h2> <h2 class="text-2xl font-bold text-ghost-100 mb-2">Event Not Found</h2>
<p class="text-ghost-300 mb-6"> <p class="text-ghost-300 mb-6">
The event you're looking for doesn't exist. The event you're looking for doesn't exist.
@ -70,37 +66,25 @@
<!-- Event Meta Info --> <!-- Event Meta Info -->
<div class="bg-ghost-800 rounded-xl p-6 mb-8 border border-ghost-700"> <div class="bg-ghost-800 rounded-xl p-6 mb-8 border border-ghost-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="flex items-center space-x-3"> <div>
<Icon <p class="text-sm text-ghost-400">Date</p>
name="heroicons:calendar-days" <p class="font-semibold text-ghost-100">
class="w-6 h-6 text-blue-400" {{ formatDate(event.startDate) }}
/> </p>
<div>
<p class="text-sm text-ghost-400">Date</p>
<p class="font-semibold text-ghost-100">
{{ formatDate(event.startDate) }}
</p>
</div>
</div> </div>
<div class="flex items-center space-x-3"> <div>
<Icon name="heroicons:clock" class="w-6 h-6 text-blue-400" /> <p class="text-sm text-ghost-400">Time</p>
<div> <p class="font-semibold text-ghost-100">
<p class="text-sm text-ghost-400">Time</p> {{ formatTime(event.startDate, event.endDate) }}
<p class="font-semibold text-ghost-100"> </p>
{{ formatTime(event.startDate, event.endDate) }}
</p>
</div>
</div> </div>
<div class="flex items-center space-x-3"> <div>
<Icon name="heroicons:map-pin" class="w-6 h-6 text-blue-400" /> <p class="text-sm text-ghost-400">Location</p>
<div> <p class="font-semibold text-ghost-100">
<p class="text-sm text-ghost-400">Location</p> {{ event.location }}
<p class="font-semibold text-ghost-100"> </p>
{{ event.location }}
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -108,24 +92,16 @@
<!-- Event Cancelled Notice --> <!-- Event Cancelled Notice -->
<div v-if="event.isCancelled" class="mb-8"> <div v-if="event.isCancelled" class="mb-8">
<div class="p-6 bg-red-900/20 rounded-xl border border-red-800"> <div class="p-6 bg-red-900/20 rounded-xl border border-red-800">
<div class="flex items-start"> <h3 class="text-lg font-semibold text-red-300 mb-2">
<Icon Event Cancelled
name="heroicons:exclamation-triangle" </h3>
class="w-6 h-6 text-red-400 mr-3 mt-0.5" <p class="text-red-400" v-if="event.cancellationMessage">
/> {{ event.cancellationMessage }}
<div> </p>
<h3 class="text-lg font-semibold text-red-300 mb-2"> <p class="text-red-400" v-else>
Event Cancelled This event has been cancelled. We apologize for any
</h3> inconvenience.
<p class="text-red-400" v-if="event.cancellationMessage"> </p>
{{ event.cancellationMessage }}
</p>
<p class="text-red-400" v-else>
This event has been cancelled. We apologize for any
inconvenience.
</p>
</div>
</div>
</div> </div>
</div> </div>
@ -134,10 +110,6 @@
<div <div
class="inline-flex items-center px-4 py-2 bg-purple-100 dark:bg-purple-900/30 rounded-full" class="inline-flex items-center px-4 py-2 bg-purple-100 dark:bg-purple-900/30 rounded-full"
> >
<Icon
name="heroicons:lock-closed"
class="w-5 h-5 text-purple-600 dark:text-purple-400 mr-2"
/>
<span <span
class="text-sm font-medium text-purple-700 dark:text-purple-300" class="text-sm font-medium text-purple-700 dark:text-purple-300"
> >
@ -152,7 +124,6 @@
class="mb-8" class="mb-8"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Icon name="heroicons:user-group" class="w-5 h-5 text-blue-400" />
<span class="text-sm font-medium text-ghost-200" <span class="text-sm font-medium text-ghost-200"
>Recommended for:</span >Recommended for:</span
> >
@ -210,14 +181,6 @@
:key="speaker.name" :key="speaker.name"
class="flex items-start space-x-4" class="flex items-start space-x-4"
> >
<div
class="w-16 h-16 bg-ghost-700 rounded-full flex items-center justify-center"
>
<Icon
name="heroicons:user"
class="w-8 h-8 text-ghost-500"
/>
</div>
<div> <div>
<p class="font-semibold text-ghost-100"> <p class="font-semibold text-ghost-100">
{{ speaker.name }} {{ speaker.name }}
@ -249,19 +212,13 @@
class="p-4 bg-green-900/20 rounded-lg border border-green-800" class="p-4 bg-green-900/20 rounded-lg border border-green-800"
> >
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div class="flex items-center"> <div>
<Icon <p class="font-semibold text-green-300">
name="heroicons:check-circle" You're registered!
class="w-6 h-6 text-green-400 mr-3" </p>
/> <p class="text-sm text-green-400">
<div> We've sent a confirmation to your email
<p class="font-semibold text-green-300"> </p>
You're registered!
</p>
<p class="text-sm text-green-400">
We've sent a confirmation to your email
</p>
</div>
</div> </div>
<UButton <UButton
color="red" color="red"
@ -286,28 +243,19 @@
class="mb-6" class="mb-6"
> >
<div <div
class="flex items-start p-4 bg-amber-900/20 rounded-lg border border-amber-800" class="p-4 bg-amber-900/20 rounded-lg border border-amber-800"
> >
<Icon <p class="font-semibold text-amber-300">Membership Required</p>
name="heroicons:exclamation-triangle" <p class="text-sm text-amber-400 mt-1">
class="w-6 h-6 text-amber-400 mr-3 mt-0.5" This event is exclusive to Ghost Guild members. Join any
/> circle to gain access.
<div> </p>
<p class="font-semibold text-amber-300"> <NuxtLink
Membership Required to="/join"
</p> class="inline-flex items-center text-sm font-medium text-amber-300 hover:underline mt-2"
<p class="text-sm text-amber-400 mt-1"> >
This event is exclusive to Ghost Guild members. Join any Become a member
circle to gain access. </NuxtLink>
</p>
<NuxtLink
to="/join"
class="inline-flex items-center text-sm font-medium text-amber-300 hover:underline mt-2"
>
Become a member
<Icon name="heroicons:arrow-right" class="w-4 h-4 ml-1" />
</NuxtLink>
</div>
</div> </div>
</div> </div>
@ -417,9 +365,8 @@
</p> </p>
<a <a
href="mailto:events@ghostguild.org" href="mailto:events@ghostguild.org"
class="inline-flex items-center text-blue-400 hover:underline" class="text-blue-400 hover:underline"
> >
<Icon name="heroicons:envelope" class="w-4 h-4 mr-2" />
events@ghostguild.org events@ghostguild.org
</a> </a>
</div> </div>

View file

@ -430,9 +430,10 @@
Join the community Join the community
</h3> </h3>
<p class="text-[--ui-text]"> <p class="text-[--ui-text]">
Get instant access to everything. Fill out your profile, agree Get access to everything. Fill out your profile, agree to our
to our community guidelines, and complete payment (if community guidelines, and complete payment (if applicable).
applicable). You'll get instant access to our community. You'll get access to our community as soon as we review your
application.
</p> </p>
</div> </div>
</div> </div>

View file

@ -99,9 +99,6 @@
block block
title="Coming soon" title="Coming soon"
> >
<template #leading>
<Icon name="heroicons:calendar-days" class="w-5 h-5" />
</template>
Propose an Event Propose an Event
</UButton> </UButton>
@ -112,9 +109,6 @@
block block
title="Coming soon" title="Coming soon"
> >
<template #leading>
<Icon name="heroicons:user-group" class="w-5 h-5" />
</template>
Book a Peer Session Book a Peer Session
</UButton> </UButton>
@ -125,9 +119,6 @@
block block
title="Coming soon" title="Coming soon"
> >
<template #leading>
<Icon name="heroicons:book-open" class="w-5 h-5" />
</template>
Browse Resources Browse Resources
</UButton> </UButton>
@ -137,130 +128,38 @@
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start" class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block block
> >
<template #leading>
<Icon name="heroicons:user-circle" class="w-5 h-5" />
</template>
Update Profile Update Profile
</UButton> </UButton>
<UButton
to="/events"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
View Events
</UButton>
<UButton
to="/members"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
Browse Members
</UButton>
<UButton
to="/member/profile#account"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
Manage Account
</UButton>
</div> </div>
</UCard> </UCard>
<!-- Quick Actions Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<UCard
:ui="{
root: 'bg-ghost-900 border border-ghost-700 hover:border-whisper-600 transition-colors',
header: 'border-b-0',
body: 'bg-ghost-900',
footer: 'border-t-0 bg-ghost-900',
}"
class="hover:border-whisper-600 transition-colors"
>
<template #header>
<div
class="w-12 h-12 bg-ghost-800 border border-ghost-600 flex items-center justify-center"
>
<Icon
name="heroicons:calendar-days"
class="w-6 h-6 text-whisper-400"
/>
</div>
</template>
<h3 class="text-lg font-semibold mb-2 text-ghost-100">
Upcoming Events
</h3>
<p class="text-ghost-300 mb-4">
Discover and register for community events and workshops.
</p>
<template #footer>
<UButton
to="/events"
variant="outline"
size="sm"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
View Events
</UButton>
</template>
</UCard>
<UCard
:ui="{
root: 'bg-ghost-900 border border-ghost-700 hover:border-whisper-600 transition-colors',
header: 'border-b-0',
body: 'bg-ghost-900',
footer: 'border-t-0 bg-ghost-900',
}"
>
<template #header>
<div
class="w-12 h-12 bg-ghost-800 border border-ghost-600 flex items-center justify-center"
>
<Icon
name="heroicons:user-group"
class="w-6 h-6 text-whisper-400"
/>
</div>
</template>
<h3 class="text-lg font-semibold mb-2 text-ghost-100">Community</h3>
<p class="text-ghost-300 mb-4">
Connect with other members in your circle and beyond.
</p>
<template #footer>
<UButton
to="/members"
variant="outline"
size="sm"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
Browse Members
</UButton>
</template>
</UCard>
<UCard
:ui="{
root: 'bg-ghost-900 border border-ghost-700 hover:border-whisper-600 transition-colors',
header: 'border-b-0',
body: 'bg-ghost-900',
footer: 'border-t-0 bg-ghost-900',
}"
>
<template #header>
<div
class="w-12 h-12 bg-ghost-800 border border-ghost-600 flex items-center justify-center"
>
<Icon
name="heroicons:cog-6-tooth"
class="w-6 h-6 text-whisper-400"
/>
</div>
</template>
<h3 class="text-lg font-semibold mb-2 text-ghost-100">
Account Settings
</h3>
<p class="text-ghost-300 mb-4">
Manage your profile and membership settings.
</p>
<template #footer>
<UButton
to="/member/profile#account"
variant="outline"
size="sm"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
Manage Account
</UButton>
</template>
</UCard>
</div>
<!-- Your Registered Events --> <!-- Your Registered Events -->
<UCard <UCard
:ui="{ :ui="{

View file

@ -429,28 +429,12 @@
<UFormField <UFormField
label="Conversational Support Topics" label="Conversational Support Topics"
name="peerSupportSupportTopics" name="peerSupportSupportTopics"
description="Select emotional and conversational support areas"
> >
<div class="space-y-2 mt-2"> <UCheckboxGroup
<label v-model="formData.peerSupportSupportTopics"
v-for="topic in availableSupportTopics" :items="availableSupportTopics"
:key="topic" color="primary"
class="flex items-center gap-3 p-3 rounded-lg border border-ghost-700 hover:border-purple-500/50 transition-colors cursor-pointer" />
:class="
formData.peerSupportSupportTopics.includes(topic)
? 'bg-purple-500/10 border-purple-500/50'
: 'bg-ghost-900/50'
"
>
<input
type="checkbox"
:value="topic"
v-model="formData.peerSupportSupportTopics"
class="rounded border-ghost-600 text-purple-500 focus:ring-purple-500 focus:ring-offset-0 bg-ghost-800"
/>
<span class="text-ghost-200">{{ topic }}</span>
</label>
</div>
</UFormField> </UFormField>
<!-- Availability --> <!-- Availability -->
@ -927,6 +911,9 @@ const hasChanges = computed(() => {
// Offering tag management // Offering tag management
const addOfferingTag = () => { const addOfferingTag = () => {
const tag = currentOfferingTagInput.value.trim().replace(/,$/, ""); const tag = currentOfferingTagInput.value.trim().replace(/,$/, "");
if (!Array.isArray(formData.offering.tags)) {
formData.offering.tags = [];
}
if (tag && !formData.offering.tags.includes(tag)) { if (tag && !formData.offering.tags.includes(tag)) {
formData.offering.tags.push(tag); formData.offering.tags.push(tag);
currentOfferingTagInput.value = ""; currentOfferingTagInput.value = "";
@ -934,12 +921,16 @@ const addOfferingTag = () => {
}; };
const removeOfferingTag = (index) => { const removeOfferingTag = (index) => {
if (!formData.offering.tags) return;
formData.offering.tags.splice(index, 1); formData.offering.tags.splice(index, 1);
}; };
// Looking For tag management // Looking For tag management
const addLookingForTag = () => { const addLookingForTag = () => {
const tag = currentLookingForTagInput.value.trim().replace(/,$/, ""); const tag = currentLookingForTagInput.value.trim().replace(/,$/, "");
if (!Array.isArray(formData.lookingFor.tags)) {
formData.lookingFor.tags = [];
}
if (tag && !formData.lookingFor.tags.includes(tag)) { if (tag && !formData.lookingFor.tags.includes(tag)) {
formData.lookingFor.tags.push(tag); formData.lookingFor.tags.push(tag);
currentLookingForTagInput.value = ""; currentLookingForTagInput.value = "";
@ -947,6 +938,7 @@ const addLookingForTag = () => {
}; };
const removeLookingForTag = (index) => { const removeLookingForTag = (index) => {
if (!formData.lookingFor.tags) return;
formData.lookingFor.tags.splice(index, 1); formData.lookingFor.tags.splice(index, 1);
}; };
@ -964,6 +956,9 @@ const removePeerSkillTopic = (index) => {
}; };
const addSuggestedSkillTopic = (tag) => { const addSuggestedSkillTopic = (tag) => {
if (!Array.isArray(formData.peerSupportSkillTopics)) {
formData.peerSupportSkillTopics = [];
}
if (!formData.peerSupportSkillTopics.includes(tag)) { if (!formData.peerSupportSkillTopics.includes(tag)) {
formData.peerSupportSkillTopics.push(tag); formData.peerSupportSkillTopics.push(tag);
} }
@ -982,22 +977,32 @@ const loadProfile = () => {
// Load offering (handle both old string and new object format) // Load offering (handle both old string and new object format)
if (typeof memberData.value.offering === "string") { if (typeof memberData.value.offering === "string") {
formData.offering = { text: memberData.value.offering, tags: [] }; 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 { } else {
formData.offering = { formData.offering.text = "";
text: memberData.value.offering?.text || "", formData.offering.tags = [];
tags: memberData.value.offering?.tags || [],
};
} }
// Load lookingFor (handle both old string and new object format) // Load lookingFor (handle both old string and new object format)
if (typeof memberData.value.lookingFor === "string") { if (typeof memberData.value.lookingFor === "string") {
formData.lookingFor = { text: memberData.value.lookingFor, tags: [] }; 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 { } else {
formData.lookingFor = { formData.lookingFor.text = "";
text: memberData.value.lookingFor?.text || "", formData.lookingFor.tags = [];
tags: memberData.value.lookingFor?.tags || [],
};
} }
formData.showInDirectory = memberData.value.showInDirectory ?? true; formData.showInDirectory = memberData.value.showInDirectory ?? true;
@ -1006,16 +1011,29 @@ const loadProfile = () => {
if (memberData.value.peerSupport) { if (memberData.value.peerSupport) {
formData.peerSupportEnabled = formData.peerSupportEnabled =
memberData.value.peerSupport.enabled || false; memberData.value.peerSupport.enabled || false;
formData.peerSupportSkillTopics = formData.peerSupportSkillTopics = Array.isArray(
memberData.value.peerSupport.skillTopics || []; memberData.value.peerSupport.skillTopics,
formData.peerSupportSupportTopics = )
memberData.value.peerSupport.supportTopics || []; ? [...memberData.value.peerSupport.skillTopics]
: [];
formData.peerSupportSupportTopics = Array.isArray(
memberData.value.peerSupport.supportTopics,
)
? [...memberData.value.peerSupport.supportTopics]
: [];
formData.peerSupportAvailability = formData.peerSupportAvailability =
memberData.value.peerSupport.availability || ""; memberData.value.peerSupport.availability || "";
formData.peerSupportMessage = formData.peerSupportMessage =
memberData.value.peerSupport.personalMessage || ""; memberData.value.peerSupport.personalMessage || "";
formData.peerSupportSlackUsername = formData.peerSupportSlackUsername =
memberData.value.peerSupport.slackUsername || ""; memberData.value.peerSupport.slackUsername || "";
} else {
formData.peerSupportEnabled = false;
formData.peerSupportSkillTopics = [];
formData.peerSupportSupportTopics = [];
formData.peerSupportAvailability = "";
formData.peerSupportMessage = "";
formData.peerSupportSlackUsername = "";
} }
// Load privacy settings (with defaults) // Load privacy settings (with defaults)

View file

@ -119,8 +119,8 @@
<!-- Active Filters --> <!-- Active Filters -->
<div <div
v-if=" v-if="
selectedCircle || (selectedCircle && selectedCircle !== 'all') ||
peerSupportFilter || (peerSupportFilter && peerSupportFilter !== 'all') ||
selectedSkills.length > 0 || selectedSkills.length > 0 ||
selectedTopics.length > 0 selectedTopics.length > 0
" "
@ -128,7 +128,7 @@
> >
<span class="text-ghost-400">Active filters:</span> <span class="text-ghost-400">Active filters:</span>
<span <span
v-if="selectedCircle" v-if="selectedCircle && selectedCircle !== 'all'"
class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1" class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1"
> >
{{ circleLabels[selectedCircle] }} {{ circleLabels[selectedCircle] }}
@ -141,7 +141,7 @@
</button> </button>
</span> </span>
<span <span
v-if="peerSupportFilter" v-if="peerSupportFilter && peerSupportFilter !== 'all'"
class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1" class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1"
> >
Offering Peer Support Offering Peer Support
@ -187,8 +187,14 @@
<div <div
v-for="member in members" v-for="member in members"
:key="member._id" :key="member._id"
class="backdrop-blur-sm bg-ghost-900/50 border border-ghost-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group" class="relative backdrop-blur-sm bg-ghost-900/50 border border-ghost-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
> >
<!-- Peer Support Sticker Badge -->
<PeerSupportBadge
v-if="member.peerSupport?.enabled"
type="sticker"
/>
<!-- Header Section --> <!-- Header Section -->
<div class="flex items-start gap-4 mb-4"> <div class="flex items-start gap-4 mb-4">
<!-- Avatar --> <!-- Avatar -->
@ -207,18 +213,15 @@
<!-- Name and Meta Info --> <!-- Name and Meta Info -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 flex-wrap mb-2"> <div class="flex items-baseline gap-2 flex-wrap mb-2">
<NuxtLink <h3 class="font-semibold text-lg text-ghost-100">
:to="`/updates/user/${member._id}`"
class="font-semibold text-lg text-ghost-100 hover:text-purple-300 transition-colors"
>
{{ member.name }} {{ member.name }}
</NuxtLink> </h3>
<span v-if="member.pronouns" class="text-sm text-ghost-400"> <span v-if="member.pronouns" class="text-sm text-ghost-400">
{{ member.pronouns }} {{ member.pronouns }}
</span> </span>
</div> </div>
<div class="flex items-center gap-2 flex-wrap mb-2"> <div class="flex items-center gap-2 flex-wrap">
<span <span
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30" class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30"
> >
@ -234,114 +237,25 @@
🕐 {{ member.timeZone }} 🕐 {{ member.timeZone }}
</span> </span>
</div> </div>
<!-- Social Links -->
<div
v-if="
member.socialLinks && hasSocialLinks(member.socialLinks)
"
class="flex gap-3"
>
<a
v-if="member.socialLinks.mastodon"
:href="member.socialLinks.mastodon"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="Mastodon"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M23.193 7.879c0-5.206-3.411-6.732-3.411-6.732C18.062.357 15.108.025 12.041 0h-.076c-3.068.025-6.02.357-7.74 1.147 0 0-3.411 1.526-3.411 6.732 0 1.192-.023 2.618.015 4.129.124 5.092.934 10.109 5.641 11.355 2.17.574 4.034.695 5.535.612 2.722-.15 4.25-.972 4.25-.972l-.09-1.975s-1.945.613-4.129.539c-2.165-.074-4.449-.233-4.799-2.891a5.499 5.499 0 0 1-.048-.745s2.125.52 4.817.643c1.646.075 3.19-.097 4.758-.283 3.007-.359 5.625-2.212 5.954-3.905.517-2.665.475-6.507.475-6.507zm-4.024 6.709h-2.497V8.469c0-1.29-.543-1.944-1.628-1.944-1.2 0-1.802.776-1.802 2.312v3.349h-2.483v-3.35c0-1.536-.602-2.312-1.802-2.312-1.085 0-1.628.655-1.628 1.944v6.119H4.832V8.284c0-1.289.328-2.313.987-3.07.68-.758 1.569-1.146 2.674-1.146 1.278 0 2.246.491 2.886 1.474L12 6.585l.622-1.043c.64-.983 1.608-1.474 2.886-1.474 1.104 0 1.994.388 2.674 1.146.658.757.986 1.781.986 3.07v6.304z"
/>
</svg>
</a>
<a
v-if="member.socialLinks.linkedin"
:href="member.socialLinks.linkedin"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="LinkedIn"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
/>
</svg>
</a>
<a
v-if="member.socialLinks.website"
:href="member.socialLinks.website"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="Website"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
/>
</svg>
</a>
<a
v-if="member.socialLinks.other"
:href="member.socialLinks.other"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="Other link"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</a>
</div>
</div> </div>
</div> </div>
<!-- Bio --> <!-- Bio -->
<div v-if="member.bio" class="mb-4"> <div
<p class="text-ghost-300 text-sm leading-relaxed"> v-if="member.bio"
{{ member.bio }} class="mb-4 text-ghost-300 text-sm leading-relaxed prose prose-invert prose-sm max-w-none"
</p> v-html="renderMarkdown(member.bio)"
</div> ></div>
<!-- Peer Support Section --> <!-- Peer Support Section -->
<div <div
v-if="member.peerSupport?.enabled" v-if="member.peerSupport?.enabled"
class="mb-4 p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg" class="mb-4 p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg"
> >
<div class="flex items-center gap-2 mb-2"> <div class="mb-3">
<span class="text-purple-300 font-medium text-sm"> <p class="text-purple-300 font-medium text-sm mb-2">
💜 Offering Peer Support {{ member.name }} offers 1:1 chats on:
</span> </p>
</div> </div>
<!-- Topics --> <!-- Topics -->
@ -379,74 +293,85 @@
Availability: {{ member.peerSupport.availability }} Availability: {{ member.peerSupport.availability }}
</div> </div>
<!-- Contact Button --> <!-- Contact Section -->
<a <div v-if="member.peerSupport.slackUsername" class="space-y-2">
v-if="member.peerSupport.slackUsername" <p class="text-sm text-purple-300 font-medium">
:href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`" Book a Peer Support call now:
@click.prevent="openSlackDM(member)" </p>
class="inline-block px-3 py-1.5 bg-purple-500/20 text-purple-300 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors text-sm font-medium cursor-pointer" <a
> :href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`"
Message {{ member.peerSupport.slackUsername }} on Slack @click.prevent="openSlackDM(member)"
</a> class="inline-block px-3 py-1.5 bg-purple-500/20 text-purple-300 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors text-sm font-medium cursor-pointer"
>
Message {{ member.peerSupport.slackUsername }} on Slack
</a>
</div>
</div> </div>
<!-- Offering and Looking For --> <!-- Offering and Looking For -->
<div <div
v-if="member.offering || member.lookingFor" v-if="member.offering || member.lookingFor"
class="grid grid-cols-1 md:grid-cols-2 gap-4" class="space-y-4"
> >
<!-- Offering --> <h4
<div v-if="member.offering" class="space-y-2"> class="text-sm font-semibold text-purple-300 uppercase tracking-wide"
<h4 class="text-xs font-semibold text-purple-400 uppercase"> >
Offering Skills Exchange
</h4> </h4>
<p <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
v-if="member.offering.description" <!-- Offering -->
class="text-ghost-300 text-sm" <div v-if="member.offering" class="space-y-2">
> <h5 class="text-xs font-semibold text-purple-400 uppercase">
{{ member.offering.description }} Can share
</p> </h5>
<div <p
v-if=" v-if="member.offering.description"
member.offering.tags && member.offering.tags.length > 0 class="text-ghost-300 text-sm"
"
class="flex flex-wrap gap-1"
>
<span
v-for="tag in member.offering.tags"
:key="tag"
class="px-2 py-0.5 bg-green-500/20 text-green-300 rounded text-xs border border-green-500/30"
> >
{{ tag }} {{ member.offering.description }}
</span> </p>
<div
v-if="
member.offering.tags && member.offering.tags.length > 0
"
class="flex flex-wrap gap-1"
>
<span
v-for="tag in member.offering.tags"
:key="tag"
class="px-2 py-0.5 bg-green-500/20 text-green-300 rounded text-xs border border-green-500/30"
>
{{ tag }}
</span>
</div>
</div> </div>
</div>
<!-- Looking For --> <!-- Looking For -->
<div v-if="member.lookingFor" class="space-y-2"> <div v-if="member.lookingFor" class="space-y-2">
<h4 class="text-xs font-semibold text-purple-400 uppercase"> <h5 class="text-xs font-semibold text-purple-400 uppercase">
Looking For Looking to learn
</h4> </h5>
<p <p
v-if="member.lookingFor.description" v-if="member.lookingFor.description"
class="text-ghost-300 text-sm" class="text-ghost-300 text-sm"
>
{{ member.lookingFor.description }}
</p>
<div
v-if="
member.lookingFor.tags &&
member.lookingFor.tags.length > 0
"
class="flex flex-wrap gap-1"
>
<span
v-for="tag in member.lookingFor.tags"
:key="tag"
class="px-2 py-0.5 bg-blue-500/20 text-blue-300 rounded text-xs border border-blue-500/30"
> >
{{ tag }} {{ member.lookingFor.description }}
</span> </p>
<div
v-if="
member.lookingFor.tags &&
member.lookingFor.tags.length > 0
"
class="flex flex-wrap gap-1"
>
<span
v-for="tag in member.lookingFor.tags"
:key="tag"
class="px-2 py-0.5 bg-blue-500/20 text-blue-300 rounded text-xs border border-blue-500/30"
>
{{ tag }}
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -502,6 +427,7 @@
<script setup> <script setup>
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const { render: renderMarkdown } = useMarkdown();
// State // State
const members = ref([]); const members = ref([]);
@ -610,10 +536,12 @@ const toggleTopic = (topic) => {
// Clear filters // Clear filters
const clearCircleFilter = () => { const clearCircleFilter = () => {
selectedCircle.value = "all"; selectedCircle.value = "all";
loadMembers();
}; };
const clearPeerSupportFilter = () => { const clearPeerSupportFilter = () => {
peerSupportFilter.value = "all"; peerSupportFilter.value = "all";
loadMembers();
}; };
const clearAllFilters = () => { const clearAllFilters = () => {

43
package-lock.json generated
View file

@ -19,6 +19,7 @@
"cloudinary": "^2.7.0", "cloudinary": "^2.7.0",
"eslint": "^9.34.0", "eslint": "^9.34.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"marked": "^16.4.0",
"mongoose": "^8.18.0", "mongoose": "^8.18.0",
"nitro-cors": "^0.7.1", "nitro-cors": "^0.7.1",
"nuxt": "^4.0.3", "nuxt": "^4.0.3",
@ -28,6 +29,9 @@
"vue-cal": "^5.0.1-rc.28", "vue-cal": "^5.0.1-rc.28",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"zod": "^4.1.3" "zod": "^4.1.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19"
} }
}, },
"node_modules/@ai-sdk/gateway": { "node_modules/@ai-sdk/gateway": {
@ -5234,6 +5238,33 @@
"tailwindcss": "4.1.13" "tailwindcss": "4.1.13"
} }
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tailwindcss/vite": { "node_modules/@tailwindcss/vite": {
"version": "4.1.13", "version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz",
@ -11598,6 +11629,18 @@
"source-map-js": "^1.2.0" "source-map-js": "^1.2.0"
} }
}, },
"node_modules/marked": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.0.tgz",
"integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

View file

@ -22,6 +22,7 @@
"cloudinary": "^2.7.0", "cloudinary": "^2.7.0",
"eslint": "^9.34.0", "eslint": "^9.34.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"marked": "^16.4.0",
"mongoose": "^8.18.0", "mongoose": "^8.18.0",
"nitro-cors": "^0.7.1", "nitro-cors": "^0.7.1",
"nuxt": "^4.0.3", "nuxt": "^4.0.3",
@ -31,5 +32,8 @@
"vue-cal": "^5.0.1-rc.28", "vue-cal": "^5.0.1-rc.28",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"zod": "^4.1.3" "zod": "^4.1.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19"
} }
} }