Enhance application structure: Add runtime configuration for environment variables, integrate new dependencies for Cloudinary and UI components, and refactor member management features including improved forms and member dashboard. Update styles and layout for better user experience.

This commit is contained in:
Jennie Robinson Faber 2025-08-27 16:49:51 +01:00
parent 6e7e27ac4e
commit e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions

View file

@ -0,0 +1,229 @@
<template>
<div>
<div class="bg-white border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<h1 class="text-2xl font-bold text-gray-900">Member Management</h1>
<p class="text-gray-600">Manage Ghost Guild members, their contributions, and access levels</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Search and Actions -->
<div class="mb-6 flex justify-between items-center">
<div class="flex gap-4 items-center">
<input v-model="searchQuery" placeholder="Search members..." class="border border-gray-300 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
<select v-model="circleFilter" class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">All Circles</option>
<option value="community">Community</option>
<option value="founder">Founder</option>
<option value="practitioner">Practitioner</option>
</select>
</div>
<button @click="showCreateModal = true" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Add Member
</button>
</div>
<!-- Members Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div v-if="pending" class="p-8 text-center">
<div class="inline-flex items-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"></div>
Loading members...
</div>
</div>
<div v-else-if="error" class="p-8 text-center text-red-600">
Error loading members: {{ error }}
</div>
<table v-else class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Circle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contribution</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slack Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="member in filteredMembers" :key="member._id" class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ member.name }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-600">{{ member.email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getCircleClasses(member.circle)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
{{ member.circle }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
${{ member.contributionTier }}/month
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="member.slackInvited ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
{{ member.slackInvited ? 'Invited' : 'Pending' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{{ formatDate(member.createdAt) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<div class="flex gap-2">
<button @click="sendSlackInvite(member)" class="text-blue-600 hover:text-blue-900">Slack Invite</button>
<button @click="editMember(member)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="!pending && !error && filteredMembers.length === 0" class="p-8 text-center text-gray-500">
No members found matching your criteria
</div>
</div>
</div>
<!-- Create Member Modal -->
<div v-if="showCreateModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">Add New Member</h3>
</div>
<form @submit.prevent="createMember" class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input v-model="newMember.name" placeholder="Full name" required class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input v-model="newMember.email" type="email" placeholder="email@example.com" required class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Circle</label>
<select v-model="newMember.circle" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="community">Community</option>
<option value="founder">Founder</option>
<option value="practitioner">Practitioner</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Contribution Tier</label>
<select v-model="newMember.contributionTier" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="0">$0/month</option>
<option value="5">$5/month</option>
<option value="15">$15/month</option>
<option value="30">$30/month</option>
<option value="50">$50/month</option>
</select>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" @click="showCreateModal = false" class="px-4 py-2 text-gray-600 hover:text-gray-900">
Cancel
</button>
<button type="submit" :disabled="creating" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
{{ creating ? 'Creating...' : 'Create Member' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: 'admin'
})
const { data: members, pending, error, refresh } = await useFetch("/api/admin/members")
const searchQuery = ref('')
const circleFilter = ref('')
const showCreateModal = ref(false)
const creating = ref(false)
const newMember = reactive({
name: '',
email: '',
circle: 'community',
contributionTier: '0'
})
const filteredMembers = computed(() => {
if (!members.value) return []
return members.value.filter(member => {
const matchesSearch = !searchQuery.value ||
member.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
member.email.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesCircle = !circleFilter.value || member.circle === circleFilter.value
return matchesSearch && matchesCircle
})
})
const getCircleClasses = (circle) => {
const classes = {
community: 'bg-blue-100 text-blue-800',
founder: 'bg-purple-100 text-purple-800',
practitioner: 'bg-green-100 text-green-800'
}
return classes[circle] || 'bg-gray-100 text-gray-800'
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString()
}
const createMember = async () => {
creating.value = true
try {
await $fetch('/api/admin/members', {
method: 'POST',
body: newMember
})
showCreateModal.value = false
Object.assign(newMember, {
name: '',
email: '',
circle: 'community',
contributionTier: '0'
})
await refresh()
alert('Member created successfully!')
} catch (error) {
console.error('Failed to create member:', error)
alert('Failed to create member')
} finally {
creating.value = false
}
}
const sendSlackInvite = (member) => {
alert(`Slack invite functionality would send invite to ${member.email}`)
console.log('Send Slack invite to:', member.email)
}
const editMember = (member) => {
alert(`Edit functionality would open editor for ${member.name}`)
console.log('Edit member:', member._id)
}
</script>