cd2efe3587
Phase 1 of frontend_tasks.md - stop showing fabricated/broken UI:
- Dashboard now fetches real stats (projects, tasks, users, pending
approvals, API keys, developer stats, pending reviews, admin
activity) instead of hardcoded numbers. Added services/developer.ts
and services/review.ts wrappers for previously-unused backend
endpoints.
- Wired ActivityFeed into every project's Overview page in place of
the "coming soon" placeholder.
- Registered the missing /projects/:id/technical-specs route (view
and service already existed, just unreachable).
- Fixed AssetDeleteConfirmDialog's raw styled divs to use the shared
Alert component and wired it into AssetBrowser, matching
ShotBrowser's impact-summary + type-to-confirm safety pattern
(asset deletion was previously less safe than shot deletion).
- Fixed a shared bug in both delete dialogs where the impact-summary
section never rendered (watch on the open prop needed
{ immediate: true }).
- Replaced native confirm()/alert() with styled AlertDialog/Dialog in
NoteItem, TaskAttachments, and UserMenu's keyboard-shortcuts item.
- Removed dead-end UI: Google OAuth stub buttons, UserMenu items
pointing at non-existent routes, the /developer/docs dead link, and
the no-op action button on the API Keys placeholder page.
- Removed leftover debug console logging across 8 files.
Added frontend_report.md (full audit) and frontend_tasks.md (phased
checklist) as the reference for this and future phases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
160 lines
4.1 KiB
Vue
160 lines
4.1 KiB
Vue
<template>
|
|
<div class="w-full">
|
|
<div class="grid w-full grid-cols-5 h-auto bg-muted/50 p-1 rounded-md">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
@click="setActiveTab(tab.id)"
|
|
:class="[
|
|
'flex flex-col sm:flex-row items-center justify-center gap-1 sm:gap-2 py-2 px-1 sm:px-3 min-h-[3rem] sm:min-h-[2.5rem] transition-all duration-200 text-center rounded-sm',
|
|
activeTab === tab.id
|
|
? 'bg-background shadow-sm text-foreground'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
|
]"
|
|
>
|
|
<div class="flex items-center gap-1 sm:gap-2">
|
|
<component :is="tab.icon" class="h-4 w-4 flex-shrink-0" />
|
|
<span class="hidden sm:inline text-sm font-medium">{{ tab.label }}</span>
|
|
<span class="sm:hidden text-xs font-medium">{{ getMobileLabel(tab) }}</span>
|
|
</div>
|
|
<Badge
|
|
v-if="tab.count !== undefined"
|
|
variant="secondary"
|
|
class="text-xs hidden sm:inline-flex min-w-[1.5rem] h-5"
|
|
>
|
|
{{ tab.count }}
|
|
</Badge>
|
|
<!-- Mobile count display -->
|
|
<div
|
|
v-if="tab.count !== undefined"
|
|
class="sm:hidden text-xs text-muted-foreground font-medium"
|
|
>
|
|
{{ tab.count }}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, watch } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { LayoutDashboard, Camera, Package, ListTodo, Settings } from "lucide-vue-next";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
interface Tab {
|
|
id: string;
|
|
label: string;
|
|
icon: any;
|
|
route: string;
|
|
count?: number;
|
|
}
|
|
|
|
interface Props {
|
|
projectId: number;
|
|
shotCount?: number;
|
|
assetCount?: number;
|
|
taskCount?: number;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
// Define available tabs
|
|
const tabs = computed<Tab[]>(() => [
|
|
{
|
|
id: "overview",
|
|
label: "Overview",
|
|
icon: LayoutDashboard,
|
|
route: `/projects/${props.projectId}`,
|
|
},
|
|
{
|
|
id: "shots",
|
|
label: "Shots",
|
|
icon: Camera,
|
|
route: `/projects/${props.projectId}/shots`,
|
|
count: props.shotCount,
|
|
},
|
|
{
|
|
id: "assets",
|
|
label: "Assets",
|
|
icon: Package,
|
|
route: `/projects/${props.projectId}/assets`,
|
|
count: props.assetCount,
|
|
},
|
|
{
|
|
id: "tasks",
|
|
label: "Tasks",
|
|
icon: ListTodo,
|
|
route: `/projects/${props.projectId}/tasks`,
|
|
count: props.taskCount,
|
|
},
|
|
{
|
|
id: "settings",
|
|
label: "Settings",
|
|
icon: Settings,
|
|
route: `/projects/${props.projectId}/settings`,
|
|
},
|
|
]);
|
|
|
|
// Determine active tab based on current route
|
|
const activeTab = computed(() => {
|
|
const currentPath = route.path;
|
|
|
|
if (currentPath === `/projects/${props.projectId}`) {
|
|
return "overview";
|
|
} else if (currentPath.startsWith(`/projects/${props.projectId}/shots`)) {
|
|
return "shots";
|
|
} else if (currentPath.startsWith(`/projects/${props.projectId}/assets`)) {
|
|
return "assets";
|
|
} else if (currentPath.startsWith(`/projects/${props.projectId}/tasks`)) {
|
|
return "tasks";
|
|
} else if (
|
|
currentPath.startsWith(`/projects/${props.projectId}/settings`)
|
|
) {
|
|
return "settings";
|
|
}
|
|
|
|
return "overview";
|
|
});
|
|
|
|
// Set active tab and navigate
|
|
const setActiveTab = (tabId: string) => {
|
|
const tab = tabs.value.find((t) => t.id === tabId);
|
|
if (tab) {
|
|
router.push(tab.route).catch(() => {
|
|
// Navigation aborted (e.g. duplicate route) — safe to ignore
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get mobile label for tabs
|
|
const getMobileLabel = (tab: Tab) => {
|
|
switch (tab.id) {
|
|
case "settings":
|
|
return "Settings";
|
|
case "overview":
|
|
return "Info";
|
|
case "shots":
|
|
return "Shots";
|
|
case "assets":
|
|
return "Assets";
|
|
case "tasks":
|
|
return "Tasks";
|
|
default:
|
|
return tab.label.charAt(0);
|
|
}
|
|
};
|
|
|
|
// Watch for route changes to ensure tab state persistence
|
|
watch(
|
|
() => route.path,
|
|
() => {
|
|
// Tab state is automatically updated via activeTab computed property
|
|
// This ensures tab state persistence during project navigation
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
</script> |