monacousa-portal/components/ui/MemberCard.vue

408 lines
8.5 KiB
Vue

<template>
<div
v-motion
:initial="{ opacity: 0, scale: 0.95 }"
:enter="{
opacity: 1,
scale: 1,
transition: {
delay: delay * 50,
type: 'spring',
stiffness: 200,
damping: 20
}
}"
:hovered="{ scale: 1.02 }"
class="member-card"
:class="[
`member-card--${variant}`,
{ 'member-card--featured': featured }
]"
@click="$emit('click', member)"
>
<div class="member-card__header">
<div class="member-card__avatar">
<img
v-if="member.avatar"
:src="member.avatar"
:alt="member.name"
@error="handleImageError"
/>
<div v-else class="member-card__avatar-placeholder">
{{ initials }}
</div>
<div
v-if="member.status === 'online'"
class="member-card__status-indicator"
/>
</div>
<div v-if="member.role" class="member-card__role">
{{ member.role }}
</div>
</div>
<div class="member-card__body">
<h3 class="member-card__name">{{ member.name }}</h3>
<p v-if="member.title" class="member-card__title">{{ member.title }}</p>
<p v-if="member.company" class="member-card__company">{{ member.company }}</p>
<div v-if="member.tags && member.tags.length" class="member-card__tags">
<span
v-for="tag in member.tags.slice(0, 3)"
:key="tag"
class="member-card__tag"
>
{{ tag }}
</span>
<span
v-if="member.tags.length > 3"
class="member-card__tag member-card__tag--more"
>
+{{ member.tags.length - 3 }}
</span>
</div>
</div>
<div class="member-card__footer">
<div class="member-card__stats">
<div v-if="member.joinDate" class="member-card__stat">
<span class="member-card__stat-label">Member Since</span>
<span class="member-card__stat-value">{{ member.joinDate }}</span>
</div>
<div v-if="member.connections !== undefined" class="member-card__stat">
<span class="member-card__stat-label">Connections</span>
<span class="member-card__stat-value">{{ member.connections }}</span>
</div>
</div>
<div class="member-card__actions">
<button
class="member-card__action"
@click.stop="$emit('connect', member)"
>
<span>{{ member.connected ? '✓' : '+' }}</span>
{{ member.connected ? 'Connected' : 'Connect' }}
</button>
<button
class="member-card__action member-card__action--secondary"
@click.stop="$emit('message', member)"
>
<span>✉</span>
Message
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Member {
id: string | number
name: string
avatar?: string
title?: string
company?: string
role?: string
status?: 'online' | 'offline' | 'away'
tags?: string[]
joinDate?: string
connections?: number
connected?: boolean
}
interface Props {
member: Member
variant?: 'glass' | 'solid' | 'outline'
featured?: boolean
delay?: number
}
const props = withDefaults(defineProps<Props>(), {
variant: 'glass',
featured: false,
delay: 0
})
defineEmits<{
click: [member: Member]
connect: [member: Member]
message: [member: Member]
}>()
const initials = computed(() => {
const names = props.member.name.split(' ')
return names.map(n => n[0]).join('').toUpperCase().slice(0, 2)
})
const handleImageError = (e: Event) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
}
</script>
<style scoped lang="scss">
.member-card {
position: relative;
display: flex;
flex-direction: column;
padding: 1.5rem;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
// Glass variant
&--glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
&:hover {
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
transform: translateY(-4px);
}
}
// Solid variant
&--solid {
background: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
&:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-4px);
}
}
// Outline variant
&--outline {
background: transparent;
border: 2px solid rgba(220, 38, 38, 0.2);
&:hover {
background: rgba(220, 38, 38, 0.05);
border-color: rgba(220, 38, 38, 0.3);
transform: translateY(-4px);
}
}
// Featured state
&--featured {
border: 2px solid #dc2626;
box-shadow: 0 8px 32px rgba(220, 38, 38, 0.15);
&::before {
content: '';
position: absolute;
top: -0.5rem;
right: 1rem;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
background: #dc2626;
border-radius: 50%;
font-size: 1rem;
}
}
&__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
&__avatar {
position: relative;
width: 4rem;
height: 4rem;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 1.5rem;
font-weight: 600;
color: #dc2626;
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.1) 0%,
rgba(220, 38, 38, 0.05) 100%);
}
&__status-indicator {
position: absolute;
bottom: 0;
right: 0;
width: 1rem;
height: 1rem;
background: #10b981;
border: 2px solid white;
border-radius: 50%;
}
&__role {
padding: 0.25rem 0.75rem;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
&__body {
flex: 1;
margin-bottom: 1rem;
}
&__name {
margin: 0 0 0.25rem;
font-size: 1.125rem;
font-weight: 600;
color: #27272a;
}
&__title {
margin: 0 0 0.125rem;
font-size: 0.875rem;
font-weight: 500;
color: #dc2626;
}
&__company {
margin: 0 0 0.75rem;
font-size: 0.875rem;
color: #6b7280;
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
&__tag {
padding: 0.25rem 0.5rem;
background: rgba(220, 38, 38, 0.1);
color: #dc2626;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
&--more {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
}
}
&__footer {
padding-top: 1rem;
border-top: 1px solid rgba(220, 38, 38, 0.1);
}
&__stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
&__stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
&__stat-label {
font-size: 0.75rem;
color: #6b7280;
}
&__stat-value {
font-size: 0.875rem;
font-weight: 600;
color: #27272a;
}
&__actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
&__action {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.5rem;
background: #dc2626;
color: white;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
span {
font-size: 0.875rem;
}
&:hover {
background: #b91c1c;
transform: translateY(-1px);
}
&--secondary {
background: rgba(220, 38, 38, 0.1);
color: #dc2626;
&:hover {
background: rgba(220, 38, 38, 0.2);
}
}
}
}
// Dark mode support
@media (prefers-color-scheme: dark) {
.member-card {
&--glass {
background: rgba(30, 30, 30, 0.7);
border-color: rgba(255, 255, 255, 0.1);
}
&--solid {
background: #27272a;
}
&__name {
color: white;
}
&__stat-value {
color: #e5e5e5;
}
}
}
</style>