AlgoVision / frontend /app /components /ProjectGroupCard.vue
AlgoVision Deployer
deploy: minimal bootloader for public Space
1a25b7f
<template>
<div
class="premium-glass rounded-2xl overflow-hidden animate-fade-in-up transition-all"
:class="{ 'drop-zone-active': isDragOver }"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver"
@dragleave="onDragLeave"
@drop.prevent="onDrop"
>
<!-- Group Header -->
<div
class="flex items-center gap-3 px-5 py-4 border-b border-white/40 bg-white/20"
>
<!-- Collapse toggle -->
<button
@click="emit('toggle-collapse', group.id)"
class="w-6 h-6 flex items-center justify-center text-slate-400 hover:text-slate-600 transition-colors shrink-0"
>
<UIcon
:name="
group.collapsed ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down'
"
class="w-4 h-4 transition-transform duration-200"
/>
</button>
<!-- Folder icon -->
<UIcon
name="i-lucide-folder-open"
class="w-4 h-4 text-green-500 shrink-0"
/>
<!-- Editable group name -->
<div class="flex-1 min-w-0">
<input
v-if="isEditing"
ref="nameInput"
v-model="editName"
class="w-full bg-transparent text-slate-900 font-semibold text-sm outline-none border-b border-green-400 pb-0.5 focus:border-green-500"
@blur="commitRename"
@keydown.enter.prevent="commitRename"
@keydown.escape.prevent="cancelRename"
/>
<button
v-else
@click="startEditing"
class="text-slate-800 font-semibold text-sm truncate hover:text-green-600 transition-colors text-left w-full"
:title="group.name"
>
{{ group.name }}
</button>
</div>
<!-- Project count badge -->
<span
class="text-xs font-medium bg-slate-100 text-slate-500 px-2 py-0.5 rounded-full shrink-0"
>
{{ group.projectIds.length }}
</span>
<!-- Delete group button -->
<button
@click="onDeleteGroup"
class="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all shrink-0"
title="Delete group"
>
<UIcon name="i-lucide-trash-2" class="w-3.5 h-3.5" />
</button>
</div>
<!-- Drop zone / project grid -->
<div v-show="!group.collapsed" class="p-4 transition-all">
<!-- Empty drop zone -->
<div
v-if="projects.length === 0"
class="rounded-xl border-2 border-dashed border-slate-200 flex flex-col items-center justify-center py-8 text-slate-400 text-sm gap-2 transition-colors"
:class="
isDragOver ? 'border-green-400 bg-green-50/40 text-green-600' : ''
"
>
<UIcon
:name="isDragOver ? 'i-lucide-package-plus' : 'i-lucide-package'"
class="w-6 h-6"
/>
<span>{{ isDragOver ? "Drop to add" : "Drag projects here" }}</span>
</div>
<!-- Project cards grid -->
<div
v-else
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
:class="
isDragOver && projects.length > 0
? 'ring-2 ring-green-400 ring-offset-2 rounded-xl'
: ''
"
>
<ProjectCard
v-for="project in projects"
:key="project.id"
:project="project"
:duration="durations?.[project.id]"
@delete="(p) => emit('delete-project', p)"
@continue="(p) => emit('continue-project', p)"
@dragstart="emit('project-dragstart', $event)"
@dragend="emit('project-dragend')"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from "vue";
import type { ProjectGroup } from "~/stores/groups";
const props = defineProps<{
group: ProjectGroup;
projects: any[];
durations?: Record<string, string>;
}>();
const emit = defineEmits<{
"toggle-collapse": [groupId: string];
rename: [groupId: string, name: string];
delete: [groupId: string];
drop: [projectId: string, groupId: string];
"delete-project": [project: any];
"continue-project": [project: any];
"project-dragstart": [projectId: string];
"project-dragend": [];
}>();
// ── Drag-and-drop (counter avoids false dragleave from child elements) ──
const isDragOver = ref(false);
let dragEnterCount = 0;
function onDragEnter(e: DragEvent) {
dragEnterCount++;
isDragOver.value = true;
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
// Auto-expand collapsed group so user can see where they're dropping
if (props.group.collapsed) {
emit("toggle-collapse", props.group.id);
}
}
function onDragOver(e: DragEvent) {
isDragOver.value = true;
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
}
function onDragLeave(_e: DragEvent) {
dragEnterCount = Math.max(0, dragEnterCount - 1);
if (dragEnterCount === 0) {
isDragOver.value = false;
}
}
function onDrop(e: DragEvent) {
dragEnterCount = 0;
isDragOver.value = false;
const projectId = e.dataTransfer?.getData("text/plain");
if (projectId) {
emit("drop", projectId, props.group.id);
}
}
// ── Inline editing ──
const isEditing = ref(false);
const editName = ref("");
const nameInput = ref<HTMLInputElement | null>(null);
function startEditing() {
editName.value = props.group.name;
isEditing.value = true;
nextTick(() => {
nameInput.value?.select();
});
}
function commitRename() {
if (editName.value.trim()) {
emit("rename", props.group.id, editName.value.trim());
}
isEditing.value = false;
}
function cancelRename() {
isEditing.value = false;
}
// ── Delete ──
function onDeleteGroup() {
emit("delete", props.group.id);
}
</script>