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 "@nuxt/ui";
@plugin "@tailwindcss/typography";
@theme {
/* Font families */
--font-sans: "Inter", sans-serif;

View file

@ -157,7 +157,6 @@ const memberNavigationItems = [
{ label: "Events", path: "/events" },
{ label: "Members", path: "/members" },
{ label: "Resources", path: "/resources" },
{ label: "Peer Support", path: "/peer-support" },
{ 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"
>
<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>
<p class="text-ghost-300 mb-6">
The event you're looking for doesn't exist.
@ -70,31 +66,20 @@
<!-- Event Meta Info -->
<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="flex items-center space-x-3">
<Icon
name="heroicons:calendar-days"
class="w-6 h-6 text-blue-400"
/>
<div>
<p class="text-sm text-ghost-400">Date</p>
<p class="font-semibold text-ghost-100">
{{ formatDate(event.startDate) }}
</p>
</div>
</div>
<div class="flex items-center space-x-3">
<Icon name="heroicons:clock" class="w-6 h-6 text-blue-400" />
<div>
<p class="text-sm text-ghost-400">Time</p>
<p class="font-semibold text-ghost-100">
{{ formatTime(event.startDate, event.endDate) }}
</p>
</div>
</div>
<div class="flex items-center space-x-3">
<Icon name="heroicons:map-pin" class="w-6 h-6 text-blue-400" />
<div>
<p class="text-sm text-ghost-400">Location</p>
<p class="font-semibold text-ghost-100">
@ -103,17 +88,10 @@
</div>
</div>
</div>
</div>
<!-- Event Cancelled Notice -->
<div v-if="event.isCancelled" class="mb-8">
<div class="p-6 bg-red-900/20 rounded-xl border border-red-800">
<div class="flex items-start">
<Icon
name="heroicons:exclamation-triangle"
class="w-6 h-6 text-red-400 mr-3 mt-0.5"
/>
<div>
<h3 class="text-lg font-semibold text-red-300 mb-2">
Event Cancelled
</h3>
@ -126,18 +104,12 @@
</p>
</div>
</div>
</div>
</div>
<!-- Member-Only Badge -->
<div v-if="event.membersOnly" class="mb-8">
<div
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
class="text-sm font-medium text-purple-700 dark:text-purple-300"
>
@ -152,7 +124,6 @@
class="mb-8"
>
<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"
>Recommended for:</span
>
@ -210,14 +181,6 @@
:key="speaker.name"
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>
<p class="font-semibold text-ghost-100">
{{ speaker.name }}
@ -249,11 +212,6 @@
class="p-4 bg-green-900/20 rounded-lg border border-green-800"
>
<div class="flex items-start justify-between">
<div class="flex items-center">
<Icon
name="heroicons:check-circle"
class="w-6 h-6 text-green-400 mr-3"
/>
<div>
<p class="font-semibold text-green-300">
You're registered!
@ -262,7 +220,6 @@
We've sent a confirmation to your email
</p>
</div>
</div>
<UButton
color="red"
variant="ghost"
@ -286,16 +243,9 @@
class="mb-6"
>
<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
name="heroicons:exclamation-triangle"
class="w-6 h-6 text-amber-400 mr-3 mt-0.5"
/>
<div>
<p class="font-semibold text-amber-300">
Membership Required
</p>
<p class="font-semibold text-amber-300">Membership Required</p>
<p class="text-sm text-amber-400 mt-1">
This event is exclusive to Ghost Guild members. Join any
circle to gain access.
@ -304,12 +254,10 @@
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" />
Become a member
</NuxtLink>
</div>
</div>
</div>
<!-- Registration Form -->
<form
@ -417,9 +365,8 @@
</p>
<a
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
</a>
</div>

View file

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

View file

@ -99,9 +99,6 @@
block
title="Coming soon"
>
<template #leading>
<Icon name="heroicons:calendar-days" class="w-5 h-5" />
</template>
Propose an Event
</UButton>
@ -112,9 +109,6 @@
block
title="Coming soon"
>
<template #leading>
<Icon name="heroicons:user-group" class="w-5 h-5" />
</template>
Book a Peer Session
</UButton>
@ -125,9 +119,6 @@
block
title="Coming soon"
>
<template #leading>
<Icon name="heroicons:book-open" class="w-5 h-5" />
</template>
Browse Resources
</UButton>
@ -137,129 +128,37 @@
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
<template #leading>
<Icon name="heroicons:user-circle" class="w-5 h-5" />
</template>
Update Profile
</UButton>
</div>
</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"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
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"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
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"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
Manage Account
</UButton>
</template>
</UCard>
</div>
</UCard>
<!-- Your Registered Events -->
<UCard

View file

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

View file

@ -119,8 +119,8 @@
<!-- Active Filters -->
<div
v-if="
selectedCircle ||
peerSupportFilter ||
(selectedCircle && selectedCircle !== 'all') ||
(peerSupportFilter && peerSupportFilter !== 'all') ||
selectedSkills.length > 0 ||
selectedTopics.length > 0
"
@ -128,7 +128,7 @@
>
<span class="text-ghost-400">Active filters:</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"
>
{{ circleLabels[selectedCircle] }}
@ -141,7 +141,7 @@
</button>
</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"
>
Offering Peer Support
@ -187,8 +187,14 @@
<div
v-for="member in members"
: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 -->
<div class="flex items-start gap-4 mb-4">
<!-- Avatar -->
@ -207,18 +213,15 @@
<!-- Name and Meta Info -->
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 flex-wrap mb-2">
<NuxtLink
:to="`/updates/user/${member._id}`"
class="font-semibold text-lg text-ghost-100 hover:text-purple-300 transition-colors"
>
<h3 class="font-semibold text-lg text-ghost-100">
{{ member.name }}
</NuxtLink>
</h3>
<span v-if="member.pronouns" class="text-sm text-ghost-400">
{{ member.pronouns }}
</span>
</div>
<div class="flex items-center gap-2 flex-wrap mb-2">
<div class="flex items-center gap-2 flex-wrap">
<span
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 }}
</span>
</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>
<!-- Bio -->
<div v-if="member.bio" class="mb-4">
<p class="text-ghost-300 text-sm leading-relaxed">
{{ member.bio }}
</p>
</div>
<div
v-if="member.bio"
class="mb-4 text-ghost-300 text-sm leading-relaxed prose prose-invert prose-sm max-w-none"
v-html="renderMarkdown(member.bio)"
></div>
<!-- Peer Support Section -->
<div
v-if="member.peerSupport?.enabled"
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">
<span class="text-purple-300 font-medium text-sm">
💜 Offering Peer Support
</span>
<div class="mb-3">
<p class="text-purple-300 font-medium text-sm mb-2">
{{ member.name }} offers 1:1 chats on:
</p>
</div>
<!-- Topics -->
@ -379,9 +293,12 @@
Availability: {{ member.peerSupport.availability }}
</div>
<!-- Contact Button -->
<!-- Contact Section -->
<div v-if="member.peerSupport.slackUsername" class="space-y-2">
<p class="text-sm text-purple-300 font-medium">
Book a Peer Support call now:
</p>
<a
v-if="member.peerSupport.slackUsername"
:href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`"
@click.prevent="openSlackDM(member)"
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"
@ -389,17 +306,24 @@
Message {{ member.peerSupport.slackUsername }} on Slack
</a>
</div>
</div>
<!-- Offering and Looking For -->
<div
v-if="member.offering || member.lookingFor"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
class="space-y-4"
>
<h4
class="text-sm font-semibold text-purple-300 uppercase tracking-wide"
>
Skills Exchange
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Offering -->
<div v-if="member.offering" class="space-y-2">
<h4 class="text-xs font-semibold text-purple-400 uppercase">
Offering
</h4>
<h5 class="text-xs font-semibold text-purple-400 uppercase">
Can share
</h5>
<p
v-if="member.offering.description"
class="text-ghost-300 text-sm"
@ -424,9 +348,9 @@
<!-- Looking For -->
<div v-if="member.lookingFor" class="space-y-2">
<h4 class="text-xs font-semibold text-purple-400 uppercase">
Looking For
</h4>
<h5 class="text-xs font-semibold text-purple-400 uppercase">
Looking to learn
</h5>
<p
v-if="member.lookingFor.description"
class="text-ghost-300 text-sm"
@ -453,6 +377,7 @@
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-20">
@ -502,6 +427,7 @@
<script setup>
const { isAuthenticated } = useAuth();
const { render: renderMarkdown } = useMarkdown();
// State
const members = ref([]);
@ -610,10 +536,12 @@ const toggleTopic = (topic) => {
// Clear filters
const clearCircleFilter = () => {
selectedCircle.value = "all";
loadMembers();
};
const clearPeerSupportFilter = () => {
peerSupportFilter.value = "all";
loadMembers();
};
const clearAllFilters = () => {

43
package-lock.json generated
View file

@ -19,6 +19,7 @@
"cloudinary": "^2.7.0",
"eslint": "^9.34.0",
"jsonwebtoken": "^9.0.2",
"marked": "^16.4.0",
"mongoose": "^8.18.0",
"nitro-cors": "^0.7.1",
"nuxt": "^4.0.3",
@ -28,6 +29,9 @@
"vue-cal": "^5.0.1-rc.28",
"vue-router": "^4.5.1",
"zod": "^4.1.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19"
}
},
"node_modules/@ai-sdk/gateway": {
@ -5234,6 +5238,33 @@
"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": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz",
@ -11598,6 +11629,18 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

View file

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