feat: added group management UI
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-slate-950 p-6">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-100">Team Management</h1>
|
||||
<p class="text-slate-400 mt-1">Create and manage groups, members and permissions</p>
|
||||
</div>
|
||||
<button
|
||||
@click="showCreateModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition flex items-center gap-2"
|
||||
>
|
||||
<span class="text-xl">+</span> New Group
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
class="bg-slate-900 border border-slate-800 rounded-2xl p-6 hover:border-slate-700 transition group"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-blue-900/30 rounded-xl flex items-center justify-center text-blue-400 text-xl font-bold">
|
||||
{{ group.name.charAt(0) }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span v-if="!group.isPublic" class="px-2 py-1 bg-amber-900/30 text-amber-400 text-xs rounded-md border border-amber-900/50">Private</span>
|
||||
<span class="px-2 py-1 bg-slate-800 text-slate-400 text-xs rounded-md">{{ group.memberCount }} members</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-semibold text-slate-100 mb-2">{{ group.name }}</h3>
|
||||
<p class="text-slate-400 text-sm mb-6 line-clamp-2">{{ group.description || 'No description provided' }}</p>
|
||||
|
||||
<div class="flex items-center justify-between pt-4 border-t border-slate-800">
|
||||
<div class="flex -space-x-2">
|
||||
<div v-for="i in Math.min(group.memberCount, 3)" :key="i" class="w-8 h-8 rounded-full border-2 border-slate-900 bg-slate-800 flex items-center justify-center text-[10px] text-slate-400 font-bold">
|
||||
U{{ i }}
|
||||
</div>
|
||||
<div v-if="group.memberCount > 3" class="w-8 h-8 rounded-full border-2 border-slate-900 bg-slate-700 flex items-center justify-center text-[10px] text-slate-300 font-bold">
|
||||
+{{ group.memberCount - 3 }}
|
||||
</div>
|
||||
</div>
|
||||
<button @click="openGroupSettings(group)" class="text-blue-400 hover:text-blue-300 text-sm font-medium">Manage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Group Modal -->
|
||||
<div v-if="showCreateModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-md p-6 shadow-2xl">
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-4">Create New Group</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-400 mb-1">Group Name</label>
|
||||
<input
|
||||
v-model="newGroup.name"
|
||||
type="text"
|
||||
class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-100 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
placeholder="e.g. Development Team"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-400 mb-1">Description</label>
|
||||
<textarea
|
||||
v-model="newGroup.description"
|
||||
rows="3"
|
||||
class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-100 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
placeholder="What is this group for?"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 py-2">
|
||||
<input type="checkbox" v-model="newGroup.isPublic" id="isPublic" class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-blue-600 focus:ring-blue-500">
|
||||
<label for="isPublic" class="text-slate-300 select-none">Make group public</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-8">
|
||||
<button
|
||||
@click="showCreateModal = false"
|
||||
class="flex-1 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg font-medium transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="createGroup"
|
||||
class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ loading ? 'Creating...' : 'Create Group' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth', 'admin']
|
||||
})
|
||||
|
||||
const groups = ref([])
|
||||
const showCreateModal = ref(false)
|
||||
const loading = ref(false)
|
||||
const newGroup = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
isPublic: true
|
||||
})
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
const data = await $fetch('/api/groups')
|
||||
groups.value = data.groups
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch groups', e)
|
||||
}
|
||||
}
|
||||
|
||||
const createGroup = async () => {
|
||||
if (!newGroup.name.trim()) return
|
||||
loading.value = true
|
||||
try {
|
||||
await $fetch('/api/groups', {
|
||||
method: 'POST',
|
||||
body: newGroup
|
||||
})
|
||||
showCreateModal.value = false
|
||||
newGroup.name = ''
|
||||
newGroup.description = ''
|
||||
await fetchGroups()
|
||||
} catch (e) {
|
||||
alert('Failed to create group')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openGroupSettings = (group) => {
|
||||
// To be implemented: detailed group management
|
||||
navigateTo(`/admin/groups/${group.id}`)
|
||||
}
|
||||
|
||||
onMounted(fetchGroups)
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { profiles, canvases, follows } from '~/server/utils/db'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const userId = getRouterParam(event, 'id')
|
||||
@@ -7,34 +7,35 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, message: 'User ID is required' })
|
||||
}
|
||||
|
||||
const profile = profiles.get(userId)
|
||||
const profile = await prisma.profile.findUnique({
|
||||
where: { userId },
|
||||
include: {
|
||||
user: {
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
canvases: true,
|
||||
followers: true,
|
||||
following: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!profile) {
|
||||
// If profile doesn't exist but user might exist in Firebase,
|
||||
// we could potentially create it here or return 404
|
||||
throw createError({ statusCode: 404, message: 'Profile not found' })
|
||||
}
|
||||
|
||||
// Count user's canvases
|
||||
const userCanvases = Array.from(canvases.values()).filter(
|
||||
canvas => canvas.ownerId === userId
|
||||
)
|
||||
|
||||
// Count followers and following
|
||||
const followers = Array.from(follows.values()).filter(
|
||||
follow => follow.followingId === userId
|
||||
).length
|
||||
|
||||
const following = Array.from(follows.values()).filter(
|
||||
follow => follow.followerId === userId
|
||||
).length
|
||||
|
||||
// Update profile with current stats
|
||||
const updatedProfile = {
|
||||
...profile,
|
||||
canvasCount: userCanvases.length,
|
||||
followerCount: followers,
|
||||
followingCount: following
|
||||
return {
|
||||
profile: {
|
||||
...profile,
|
||||
canvasCount: profile.user._count.canvases,
|
||||
followerCount: profile.user._count.followers,
|
||||
followingCount: profile.user._count.following
|
||||
}
|
||||
}
|
||||
|
||||
profiles.set(userId, updatedProfile)
|
||||
|
||||
return { profile: updatedProfile }
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const { PrismaPg } = require('@prisma/adapter-pg')
|
||||
const { Pool } = require('pg')
|
||||
|
||||
// Создаем соединение через pg (как того требует Driver Adapter в Prisma 7)
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
const pool = new Pool({ connectionString })
|
||||
const adapter = new PrismaPg(pool)
|
||||
|
||||
const prisma = new PrismaClient({ adapter })
|
||||
|
||||
async function main() {
|
||||
const userId = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2' // Твой UID из лога
|
||||
|
||||
console.log('🚀 Initializing admin using Driver Adapter for UID:', userId)
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { id: userId },
|
||||
update: {
|
||||
role: 'admin'
|
||||
},
|
||||
create: {
|
||||
id: userId,
|
||||
email: 'admin@xflow.kz',
|
||||
username: 'alex_admin',
|
||||
password: 'firebase_auth',
|
||||
role: 'admin',
|
||||
profile: {
|
||||
create: {
|
||||
displayName: 'Alex Admin',
|
||||
bio: 'XFlow Infrastructure Creator',
|
||||
joinedAt: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ Success! Admin user created/updated:', user.username)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Error initializing admin:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
await pool.end()
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const { PrismaPg } = require('@prisma/adapter-pg')
|
||||
const { Pool } = require('pg')
|
||||
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
const pool = new Pool({ connectionString })
|
||||
const adapter = new PrismaPg(pool)
|
||||
const prisma = new PrismaClient({ adapter })
|
||||
|
||||
async function main() {
|
||||
const adminId = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2'
|
||||
|
||||
console.log('📦 Seeding Mindmaps to PostgreSQL...')
|
||||
|
||||
// 1. Создаем основную карту "Обзор Проекта XFlow"
|
||||
const xflowCanvas = await prisma.canvas.upsert({
|
||||
where: { id: 'xflow-project' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'xflow-project',
|
||||
name: 'Обзор Проекта XFlow',
|
||||
ownerId: adminId,
|
||||
isPublic: true,
|
||||
nodes: [
|
||||
{ id: 'arch-1', type: 'text', x: 100, y: 100, width: 320, height: 180, text: '# 🏗️ Основная Архитектура\n\n- **Nuxt 3** - Vue 3 фреймворк\n- **TailwindCSS** - Стилизация\n- **Vue Flow** - Движок канваса\n- **Firebase** - Аутентификация\n- **PostgreSQL + Prisma** - Хранение данных\n- **PWA** - Progressive Web App' },
|
||||
{ id: 'be-1', type: 'text', x: 500, y: 250, width: 280, height: 160, text: '# ⚙️ Backend API\n\n- `/api/canvas/*` - Prisma CRUD\n- `/api/groups/*` - Групповой доступ\n- `/api/admin/*` - Админка\n- **Gitea Actions** - Автодеплой' }
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1', fromNode: 'arch-1', fromSide: 'right', toNode: 'be-1', toSide: 'left' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ Success! Mindmap "Обзор Проекта XFlow" is now in the database.')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
await pool.end()
|
||||
})
|
||||
Reference in New Issue
Block a user