Wrap members directory page in PageShell. Also expand visual mask selectors to cover .filter-bar, .skills-bar, and .connections-section because filter content varies based on dynamic tag/topic state and async fetch ordering. Rebaselines several existing snapshots that now mask wider regions but capture the same structural layout.
958 lines
22 KiB
Vue
958 lines
22 KiB
Vue
<template>
|
|
<PageShell
|
|
title="Members"
|
|
:subtitle="`${totalCount} member${totalCount === 1 ? '' : 's'} across 3 circles`"
|
|
>
|
|
<!-- Filter Bar -->
|
|
<div class="filter-bar">
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
class="filter-search"
|
|
placeholder="Search members..."
|
|
@input="debouncedSearch"
|
|
/>
|
|
<select
|
|
v-model="selectedCircle"
|
|
class="filter-select"
|
|
@change="loadMembers"
|
|
>
|
|
<option
|
|
v-for="opt in circleOptions"
|
|
:key="opt.value"
|
|
:value="opt.value"
|
|
>
|
|
{{ opt.label }}
|
|
</option>
|
|
</select>
|
|
<label class="filter-toggle">
|
|
<input
|
|
type="checkbox"
|
|
:checked="peerSupportFilter === 'true'"
|
|
@change="togglePeerSupport"
|
|
/>
|
|
Offering support
|
|
</label>
|
|
<span class="filter-count"
|
|
>Showing {{ totalCount }} member{{ totalCount === 1 ? "" : "s" }}</span
|
|
>
|
|
</div>
|
|
|
|
<!-- Craft Tags Filter -->
|
|
<div
|
|
v-if="craftTagOptions.length > 0"
|
|
class="skills-bar"
|
|
>
|
|
<span class="tag-label">Craft:</span>
|
|
<button
|
|
v-for="tag in craftTagOptions.slice(
|
|
0,
|
|
showAllCraftTags ? undefined : 10,
|
|
)"
|
|
:key="tag.slug"
|
|
type="button"
|
|
class="skill-tag"
|
|
:class="{ active: selectedCraftTags.includes(tag.slug) }"
|
|
@click="toggleCraftTag(tag.slug)"
|
|
>
|
|
{{ tag.label }}
|
|
</button>
|
|
<button
|
|
v-if="craftTagOptions.length > 10"
|
|
type="button"
|
|
class="more-btn"
|
|
@click="showAllCraftTags = !showAllCraftTags"
|
|
>
|
|
{{
|
|
showAllCraftTags ? "Show less" : `+${craftTagOptions.length - 10} more`
|
|
}}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Connection Tags Filter -->
|
|
<div
|
|
v-if="connectionTagOptions.length > 0"
|
|
class="skills-bar"
|
|
>
|
|
<span class="tag-label">Topics:</span>
|
|
<button
|
|
v-for="tag in connectionTagOptions.slice(
|
|
0,
|
|
showAllConnectionTags ? undefined : 10,
|
|
)"
|
|
:key="tag.slug"
|
|
type="button"
|
|
class="skill-tag"
|
|
:class="{ active: selectedConnectionTags.includes(tag.slug) }"
|
|
@click="toggleConnectionTag(tag.slug)"
|
|
>
|
|
{{ tag.label }}
|
|
</button>
|
|
<button
|
|
v-if="connectionTagOptions.length > 10"
|
|
type="button"
|
|
class="more-btn"
|
|
@click="showAllConnectionTags = !showAllConnectionTags"
|
|
>
|
|
{{
|
|
showAllConnectionTags ? "Show less" : `+${connectionTagOptions.length - 10} more`
|
|
}}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Active Filters -->
|
|
<div v-if="hasActiveFilters" class="active-filters">
|
|
<span class="af-label">Active filters:</span>
|
|
<span v-if="selectedCircle && selectedCircle !== 'all'" class="af-tag">
|
|
{{ circleLabels[selectedCircle] }}
|
|
<button type="button" @click="clearCircleFilter">×</button>
|
|
</span>
|
|
<span
|
|
v-if="peerSupportFilter && peerSupportFilter !== 'all'"
|
|
class="af-tag"
|
|
>
|
|
Offering Peer Support
|
|
<button type="button" @click="clearPeerSupportFilter">×</button>
|
|
</span>
|
|
<span v-for="slug in selectedCraftTags" :key="'c-' + slug" class="af-tag">
|
|
{{ craftTagLabel(slug) }}
|
|
<button type="button" @click="toggleCraftTag(slug)">×</button>
|
|
</span>
|
|
<span v-for="slug in selectedConnectionTags" :key="'t-' + slug" class="af-tag">
|
|
{{ connectionTagLabel(slug) }}
|
|
<button type="button" @click="toggleConnectionTag(slug)">×</button>
|
|
</span>
|
|
<button
|
|
v-if="selectedCraftTags.length > 0 || selectedConnectionTags.length > 0"
|
|
type="button"
|
|
class="clear-all-btn"
|
|
@click="clearAllFilters"
|
|
>
|
|
Clear all
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="loading && !members.length" class="loading-state">
|
|
<p>Loading members...</p>
|
|
</div>
|
|
|
|
<!-- Member Grid -->
|
|
<div v-else-if="members.length > 0" class="member-grid">
|
|
<div v-for="member in members" :key="member._id" class="member-card">
|
|
<div class="mc-head">
|
|
<div class="mc-avatar">
|
|
<img
|
|
v-if="member.avatar"
|
|
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`"
|
|
:alt="member.name"
|
|
class="mc-avatar-img"
|
|
/>
|
|
<span v-else>{{ getInitials(member.name) }}</span>
|
|
</div>
|
|
<div class="mc-info">
|
|
<div class="mc-name">
|
|
<NuxtLink :to="`/members/${member._id}`">{{
|
|
member.name
|
|
}}</NuxtLink>
|
|
<span v-if="member.pronouns" class="mc-pronouns">{{
|
|
member.pronouns
|
|
}}</span>
|
|
</div>
|
|
<div class="mc-meta">
|
|
<span class="badge" :class="member.circle">{{
|
|
circleLabels[member.circle]
|
|
}}</span>
|
|
<template v-if="member.studio">
|
|
<span class="sep">·</span>
|
|
{{ member.studio }}
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="member.bio"
|
|
class="mc-bio"
|
|
v-html="renderMarkdown(member.bio)"
|
|
></div>
|
|
|
|
<div v-if="member.location || member.timeZone" class="mc-location">
|
|
{{
|
|
[member.location, member.timeZone].filter(Boolean).join(" \u00b7 ")
|
|
}}
|
|
</div>
|
|
|
|
<!-- Craft tags (fall back to offering.tags) -->
|
|
<div
|
|
v-if="getMemberCraftTags(member).length > 0"
|
|
class="mc-tags"
|
|
>
|
|
<span class="tag-label">Craft:</span>
|
|
<span
|
|
v-for="tag in getMemberCraftTags(member)"
|
|
:key="tag"
|
|
class="skill-tag"
|
|
>{{ craftTagLabel(tag) }}</span
|
|
>
|
|
</div>
|
|
|
|
<!-- Community connections topics (fall back to lookingFor.tags) -->
|
|
<div
|
|
v-if="getMemberConnectionTopics(member).length > 0"
|
|
class="mc-looking"
|
|
>
|
|
<span
|
|
v-for="topic in getMemberConnectionTopics(member)"
|
|
:key="topic.tagSlug || topic"
|
|
class="connection-topic"
|
|
>
|
|
<span class="connection-state">{{ stateLabel(topic.state) }}</span>
|
|
{{ connectionTagLabel(topic.tagSlug || topic) }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Peer support session link -->
|
|
<a
|
|
v-if="showPeerSupport(member)"
|
|
href="#"
|
|
class="mc-session"
|
|
@click.prevent="openSlackDM(member)"
|
|
>
|
|
Book session
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="empty-state">
|
|
<p class="empty-title">No members found</p>
|
|
<p class="empty-sub">Try adjusting your search or filters</p>
|
|
<button type="button" class="btn" @click="clearAllFilters">
|
|
Clear Filters
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Load more / count -->
|
|
<div v-if="members.length > 0" class="load-more">
|
|
<span
|
|
>Showing {{ members.length }} of {{ totalCount }} member{{
|
|
totalCount === 1 ? "" : "s"
|
|
}}</span
|
|
>
|
|
</div>
|
|
|
|
<!-- Not Authenticated Notice -->
|
|
<div v-if="!isAuthenticated && members.length > 0" class="auth-notice">
|
|
<p>Some member information is visible to members only.</p>
|
|
<div class="auth-actions">
|
|
<button
|
|
type="button"
|
|
class="btn"
|
|
@click="
|
|
openLoginModal({
|
|
title: 'Sign in to see more',
|
|
description: 'Log in to view full member profiles',
|
|
})
|
|
"
|
|
>
|
|
Log In
|
|
</button>
|
|
<NuxtLink to="/join" class="btn btn-primary">Join Ghost Guild</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</PageShell>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { isAuthenticated } = useAuth();
|
|
const { openLoginModal } = useLoginModal();
|
|
const { render: renderMarkdown } = useMarkdown();
|
|
|
|
// State
|
|
const members = ref([]);
|
|
const totalCount = ref(0);
|
|
const loading = ref(true);
|
|
const searchQuery = ref("");
|
|
const selectedCircle = ref("all");
|
|
const peerSupportFilter = ref("all");
|
|
const selectedCraftTags = ref([]);
|
|
const selectedConnectionTags = ref([]);
|
|
const showAllCraftTags = ref(false);
|
|
const showAllConnectionTags = ref(false);
|
|
|
|
// Tag options from API
|
|
const craftTagOptions = ref([]);
|
|
const connectionTagOptions = ref([]);
|
|
|
|
// State display text mapping
|
|
const stateLabels = {
|
|
help: "Can help",
|
|
interested: "Interested",
|
|
seeking: "Need help",
|
|
};
|
|
|
|
const stateLabel = (state) => stateLabels[state] || state || "";
|
|
|
|
// Circle options
|
|
const circleOptions = [
|
|
{ label: "All Circles", value: "all" },
|
|
{ label: "Community", value: "community" },
|
|
{ label: "Founder", value: "founder" },
|
|
{ label: "Practitioner", value: "practitioner" },
|
|
];
|
|
|
|
const circleLabels = {
|
|
community: "Community",
|
|
founder: "Founder",
|
|
practitioner: "Practitioner",
|
|
};
|
|
|
|
// Tag slug-to-label lookups
|
|
const craftTagLabel = (slug) => {
|
|
const found = craftTagOptions.value.find((t) => t.slug === slug);
|
|
return found ? found.label : slug;
|
|
};
|
|
|
|
const connectionTagLabel = (slug) => {
|
|
const found = connectionTagOptions.value.find((t) => t.slug === slug);
|
|
return found ? found.label : slug;
|
|
};
|
|
|
|
// Get craft tags for a member (new field, falling back to offering.tags)
|
|
const getMemberCraftTags = (member) => {
|
|
if (member.craftTags && member.craftTags.length > 0) {
|
|
return member.craftTags;
|
|
}
|
|
return member.offering?.tags || [];
|
|
};
|
|
|
|
// Get connection topics for a member (new field, falling back to lookingFor.tags)
|
|
const getMemberConnectionTopics = (member) => {
|
|
if (
|
|
member.communityConnections?.topics &&
|
|
member.communityConnections.topics.length > 0
|
|
) {
|
|
return member.communityConnections.topics;
|
|
}
|
|
// Fallback: wrap old lookingFor.tags as plain strings
|
|
if (member.lookingFor?.tags && member.lookingFor.tags.length > 0) {
|
|
return member.lookingFor.tags.map((tag) => ({ tagSlug: tag, state: null }));
|
|
}
|
|
return [];
|
|
};
|
|
|
|
// Show peer support link (check both old and new fields)
|
|
const showPeerSupport = (member) => {
|
|
if (member.communityConnections?.offerPeerSupport) return true;
|
|
if (member.peerSupport?.enabled && member.peerSupport?.slackUsername)
|
|
return true;
|
|
return false;
|
|
};
|
|
|
|
// Computed: has active filters
|
|
const hasActiveFilters = computed(() => {
|
|
return (
|
|
(selectedCircle.value && selectedCircle.value !== "all") ||
|
|
(peerSupportFilter.value && peerSupportFilter.value !== "all") ||
|
|
selectedCraftTags.value.length > 0 ||
|
|
selectedConnectionTags.value.length > 0
|
|
);
|
|
});
|
|
|
|
// Get initials from name
|
|
const getInitials = (name) => {
|
|
if (!name) return "?";
|
|
return name
|
|
.split(" ")
|
|
.map((w) => w[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
};
|
|
|
|
// Load members
|
|
const loadMembers = async () => {
|
|
loading.value = true;
|
|
|
|
try {
|
|
const params = {};
|
|
if (searchQuery.value) params.search = searchQuery.value;
|
|
if (selectedCircle.value && selectedCircle.value !== "all")
|
|
params.circle = selectedCircle.value;
|
|
if (peerSupportFilter.value && peerSupportFilter.value !== "all")
|
|
params.peerSupport = peerSupportFilter.value;
|
|
if (selectedCraftTags.value.length === 1)
|
|
params.craftTag = selectedCraftTags.value[0];
|
|
if (selectedConnectionTags.value.length === 1)
|
|
params.connectionTag = selectedConnectionTags.value[0];
|
|
|
|
const data = await $fetch("/api/members/directory", { params });
|
|
|
|
members.value = data.members || [];
|
|
totalCount.value = data.totalCount || 0;
|
|
|
|
// Update tag options from API response (only on initial load or if empty)
|
|
if (data.filters?.craftTags && craftTagOptions.value.length === 0) {
|
|
craftTagOptions.value = data.filters.craftTags;
|
|
}
|
|
if (
|
|
data.filters?.cooperativeTags &&
|
|
connectionTagOptions.value.length === 0
|
|
) {
|
|
connectionTagOptions.value = data.filters.cooperativeTags;
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load members:", error);
|
|
members.value = [];
|
|
totalCount.value = 0;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Fetch tag options from API on mount
|
|
const loadTagOptions = async () => {
|
|
try {
|
|
const data = await $fetch("/api/tags");
|
|
const tags = data.tags || [];
|
|
craftTagOptions.value = tags
|
|
.filter((t) => t.pool === "craft")
|
|
.map((t) => ({ slug: t.slug, label: t.label }));
|
|
connectionTagOptions.value = tags
|
|
.filter((t) => t.pool === "cooperative")
|
|
.map((t) => ({ slug: t.slug, label: t.label }));
|
|
} catch (error) {
|
|
console.error("Failed to load tags:", error);
|
|
}
|
|
};
|
|
|
|
// Toggle peer support checkbox
|
|
const togglePeerSupport = (e) => {
|
|
peerSupportFilter.value = e.target.checked ? "true" : "all";
|
|
loadMembers();
|
|
};
|
|
|
|
// Debounced search
|
|
let searchTimeout;
|
|
const debouncedSearch = () => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
loadMembers();
|
|
}, 300);
|
|
};
|
|
|
|
// Toggle craft tag filter
|
|
const toggleCraftTag = (slug) => {
|
|
const index = selectedCraftTags.value.indexOf(slug);
|
|
if (index > -1) {
|
|
selectedCraftTags.value.splice(index, 1);
|
|
} else {
|
|
selectedCraftTags.value = [slug]; // single-select for API query param
|
|
}
|
|
loadMembers();
|
|
};
|
|
|
|
// Toggle connection tag filter
|
|
const toggleConnectionTag = (slug) => {
|
|
const index = selectedConnectionTags.value.indexOf(slug);
|
|
if (index > -1) {
|
|
selectedConnectionTags.value.splice(index, 1);
|
|
} else {
|
|
selectedConnectionTags.value = [slug]; // single-select for API query param
|
|
}
|
|
loadMembers();
|
|
};
|
|
|
|
// Clear filters
|
|
const clearCircleFilter = () => {
|
|
selectedCircle.value = "all";
|
|
loadMembers();
|
|
};
|
|
|
|
const clearPeerSupportFilter = () => {
|
|
peerSupportFilter.value = "all";
|
|
loadMembers();
|
|
};
|
|
|
|
const clearAllFilters = () => {
|
|
searchQuery.value = "";
|
|
selectedCircle.value = "all";
|
|
peerSupportFilter.value = "all";
|
|
selectedCraftTags.value = [];
|
|
selectedConnectionTags.value = [];
|
|
loadMembers();
|
|
};
|
|
|
|
// Slack DM functionality
|
|
const openSlackDM = async (member) => {
|
|
const username =
|
|
member.communityConnections?.slackHandle ||
|
|
member.peerSupport?.slackUsername ||
|
|
member.name;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(username);
|
|
} catch (err) {
|
|
console.log("Could not copy to clipboard:", err);
|
|
}
|
|
|
|
alert(
|
|
`Opening Slack...\n\nSearch for: ${username}\n\n(Username copied to clipboard)`,
|
|
);
|
|
window.open("https://gammaspace.slack.com", "_blank");
|
|
};
|
|
|
|
// Load on mount and handle query params
|
|
onMounted(async () => {
|
|
const route = useRoute();
|
|
if (route.query.peerSupport === "true") {
|
|
peerSupportFilter.value = "true";
|
|
}
|
|
|
|
await loadTagOptions();
|
|
loadMembers();
|
|
});
|
|
|
|
useHead({
|
|
title: "Member Directory - Ghost Guild",
|
|
meta: [
|
|
{
|
|
name: "description",
|
|
content:
|
|
"Connect with members of the Ghost Guild community - game developers, founders, and practitioners building solidarity economy studios.",
|
|
},
|
|
],
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* ---- FILTER BAR ---- */
|
|
.filter-bar {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px dashed var(--border);
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-search {
|
|
font-family: "Commit Mono", monospace;
|
|
font-size: 12px;
|
|
padding: 5px 12px;
|
|
border: 1px dashed var(--border);
|
|
background: transparent;
|
|
color: var(--text);
|
|
outline: none;
|
|
min-width: 180px;
|
|
transition: border-color 0.15s;
|
|
}
|
|
.filter-search::placeholder {
|
|
color: var(--text-faint);
|
|
}
|
|
.filter-search:focus {
|
|
border-color: var(--candle-faint);
|
|
}
|
|
|
|
.filter-select {
|
|
font-family: "Commit Mono", monospace;
|
|
font-size: 11px;
|
|
padding: 5px 10px;
|
|
border: 1px dashed var(--border);
|
|
background: transparent;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%238a7e6a' stroke-width='1.2'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 8px center;
|
|
padding-right: 26px;
|
|
}
|
|
.filter-select:focus {
|
|
border-color: var(--candle-faint);
|
|
}
|
|
|
|
.filter-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
}
|
|
.filter-toggle input {
|
|
accent-color: var(--candle-dim);
|
|
}
|
|
|
|
.filter-count {
|
|
margin-left: auto;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
/* ---- SKILLS / TOPICS BAR ---- */
|
|
.skills-bar {
|
|
padding: 12px 24px;
|
|
border-bottom: 1px dashed var(--border);
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.skills-bar .tag-label {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
margin-right: 4px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
.skills-bar .skill-tag {
|
|
font-family: "Commit Mono", monospace;
|
|
font-size: 10px;
|
|
color: var(--text-dim);
|
|
padding: 2px 8px;
|
|
border: 1px dashed var(--border);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: all 0.15s;
|
|
}
|
|
.skills-bar .skill-tag:hover {
|
|
border-color: var(--candle-faint);
|
|
color: var(--text);
|
|
}
|
|
.skills-bar .skill-tag.active {
|
|
border-color: var(--candle-dim);
|
|
border-style: solid;
|
|
color: var(--candle);
|
|
background: rgba(154, 116, 32, 0.08);
|
|
}
|
|
|
|
.more-btn {
|
|
font-family: "Commit Mono", monospace;
|
|
font-size: 10px;
|
|
color: var(--candle);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 2px 4px;
|
|
}
|
|
.more-btn:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* ---- ACTIVE FILTERS ---- */
|
|
.active-filters {
|
|
padding: 10px 24px;
|
|
border-bottom: 1px dashed var(--border);
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.af-label {
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
.af-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 8px;
|
|
border: 1px dashed var(--candle-faint);
|
|
color: var(--candle);
|
|
font-size: 10px;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.af-tag button {
|
|
background: none;
|
|
border: none;
|
|
color: var(--candle);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
line-height: 1;
|
|
padding: 0 0 0 2px;
|
|
}
|
|
.af-tag button:hover {
|
|
color: var(--ember);
|
|
}
|
|
|
|
.clear-all-btn {
|
|
font-family: "Commit Mono", monospace;
|
|
font-size: 10px;
|
|
color: var(--candle);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
}
|
|
.clear-all-btn:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* ---- LOADING ---- */
|
|
.loading-state {
|
|
padding: 60px 24px;
|
|
text-align: center;
|
|
color: var(--text-faint);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ---- MEMBER GRID ---- */
|
|
.member-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0;
|
|
}
|
|
|
|
.member-card {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px dashed var(--border);
|
|
border-right: 1px dashed var(--border);
|
|
transition: background 0.15s;
|
|
}
|
|
.member-card:hover {
|
|
background: var(--surface);
|
|
}
|
|
.member-card:nth-child(2n) {
|
|
border-right: none;
|
|
}
|
|
|
|
.mc-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.mc-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--surface);
|
|
border: 1px dashed var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mc-avatar-img {
|
|
width: 28px;
|
|
height: 28px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.mc-info {
|
|
min-width: 0;
|
|
}
|
|
|
|
.mc-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-bright);
|
|
}
|
|
.mc-name a {
|
|
color: var(--text-bright);
|
|
text-decoration: none;
|
|
}
|
|
.mc-name a:hover {
|
|
color: var(--candle);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.mc-pronouns {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-left: 4px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.mc-meta {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
margin-top: 1px;
|
|
}
|
|
.mc-meta .sep {
|
|
color: var(--border);
|
|
}
|
|
|
|
.mc-bio {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
line-height: 1.6;
|
|
margin: 8px 0;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.mc-bio :deep(p) {
|
|
margin: 0;
|
|
}
|
|
|
|
.mc-location {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.mc-tags {
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
margin-top: 6px;
|
|
}
|
|
.mc-tags .tag-label {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
margin-right: 2px;
|
|
}
|
|
|
|
.mc-tags .skill-tag {
|
|
font-size: 10px;
|
|
color: var(--text-dim);
|
|
padding: 1px 6px;
|
|
border: 1px dashed var(--border);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.mc-looking {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.connection-topic {
|
|
font-size: 10px;
|
|
color: var(--text-dim);
|
|
padding: 1px 6px;
|
|
border: 1px dashed var(--border);
|
|
white-space: nowrap;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.connection-state {
|
|
font-size: 9px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
.mc-session {
|
|
display: inline-block;
|
|
margin-top: 6px;
|
|
font-family: "Commit Mono", monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
padding: 3px 10px;
|
|
border: 1px dashed var(--candle-faint);
|
|
color: var(--candle);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
background: transparent;
|
|
text-decoration: none;
|
|
}
|
|
.mc-session:hover {
|
|
border-color: var(--candle);
|
|
background: rgba(122, 90, 16, 0.06);
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* ---- LOAD MORE ---- */
|
|
.load-more {
|
|
padding: 20px 24px;
|
|
border-bottom: 1px dashed var(--border);
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
/* ---- EMPTY STATE ---- */
|
|
.empty-state {
|
|
padding: 60px 24px;
|
|
text-align: center;
|
|
}
|
|
.empty-title {
|
|
font-family: "Brygada 1918", serif;
|
|
font-size: 18px;
|
|
color: var(--text-dim);
|
|
margin-bottom: 6px;
|
|
}
|
|
.empty-sub {
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* ---- AUTH NOTICE ---- */
|
|
.auth-notice {
|
|
padding: 24px;
|
|
margin: 20px 24px;
|
|
border: 1px dashed var(--candle-faint);
|
|
text-align: center;
|
|
}
|
|
.auth-notice p {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
margin-bottom: 12px;
|
|
}
|
|
.auth-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* ---- RESPONSIVE ---- */
|
|
@media (max-width: 1024px) {
|
|
.member-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.member-card {
|
|
border-right: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.filter-bar {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
padding: 14px 20px;
|
|
}
|
|
.filter-count {
|
|
margin-left: 0;
|
|
}
|
|
.skills-bar {
|
|
padding: 10px 20px;
|
|
}
|
|
.active-filters {
|
|
padding: 8px 20px;
|
|
}
|
|
.member-card {
|
|
padding: 14px 16px;
|
|
}
|
|
.auth-notice {
|
|
margin: 16px;
|
|
}
|
|
}
|
|
</style>
|