Lots of UI fixes
This commit is contained in:
parent
1f7a0f40c0
commit
e8e3b84276
24 changed files with 3652 additions and 1770 deletions
|
|
@ -4,74 +4,134 @@
|
|||
<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>
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<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">
|
||||
<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
|
||||
: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">
|
||||
|
|
@ -79,50 +139,91 @@
|
|||
</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>
|
||||
<button
|
||||
@click="sendSlackInvite(member)"
|
||||
class="text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
Slack Invite
|
||||
</button>
|
||||
<button
|
||||
@click="editMember(member)"
|
||||
class="text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="!pending && !error && filteredMembers.length === 0" class="p-8 text-center text-gray-500">
|
||||
|
||||
<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
|
||||
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" />
|
||||
<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" />
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
|
|
@ -130,13 +231,21 @@
|
|||
<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">
|
||||
<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
|
||||
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>
|
||||
|
|
@ -147,83 +256,90 @@
|
|||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
})
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { data: members, pending, error, refresh } = await useFetch("/api/admin/members")
|
||||
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 searchQuery = ref("");
|
||||
const circleFilter = ref("");
|
||||
const showCreateModal = ref(false);
|
||||
const creating = ref(false);
|
||||
|
||||
const newMember = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
circle: 'community',
|
||||
contributionTier: '0'
|
||||
})
|
||||
name: "",
|
||||
email: "",
|
||||
circle: "community",
|
||||
contributionTier: "0",
|
||||
});
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
if (!members.value) return []
|
||||
|
||||
return members.value.filter(member => {
|
||||
const matchesSearch = !searchQuery.value ||
|
||||
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
|
||||
})
|
||||
})
|
||||
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'
|
||||
}
|
||||
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()
|
||||
}
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
const createMember = async () => {
|
||||
creating.value = true
|
||||
creating.value = true;
|
||||
try {
|
||||
await $fetch('/api/admin/members', {
|
||||
method: 'POST',
|
||||
body: newMember
|
||||
})
|
||||
|
||||
showCreateModal.value = false
|
||||
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!')
|
||||
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')
|
||||
console.error("Failed to create member:", error);
|
||||
alert("Failed to create member");
|
||||
} finally {
|
||||
creating.value = false
|
||||
creating.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendSlackInvite = (member) => {
|
||||
alert(`Slack invite functionality would send invite to ${member.email}`)
|
||||
console.log('Send Slack invite to:', member.email)
|
||||
}
|
||||
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>
|
||||
alert(`Edit functionality would open editor for ${member.name}`);
|
||||
console.log("Edit member:", member._id);
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue