Init Repo
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Table Header Actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ episodeId ? `Episode ${episodeId} Shots` : "All Shots" }}
|
||||
</h3>
|
||||
<Badge variant="secondary" v-if="shots.length > 0">
|
||||
{{ shots.length }} shot{{ shots.length !== 1 ? "s" : "" }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" @click="refreshShots">
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size="sm" @click="createShot" v-if="episodeId">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Add Shot
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"
|
||||
></div>
|
||||
<span class="text-muted-foreground">Loading shots...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="text-center py-8">
|
||||
<AlertCircle class="h-8 w-8 mx-auto text-destructive mb-2" />
|
||||
<p class="text-muted-foreground">{{ error }}</p>
|
||||
<Button variant="outline" size="sm" @click="refreshShots" class="mt-2">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="shots.length === 0" class="text-center py-12">
|
||||
<Camera class="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
||||
<h3 class="text-lg font-semibold mb-2">No shots found</h3>
|
||||
<p class="text-muted-foreground mb-4">
|
||||
{{
|
||||
episodeId
|
||||
? "This episode doesn't have any shots yet."
|
||||
: "No shots found for the selected criteria."
|
||||
}}
|
||||
</p>
|
||||
<Button @click="createShot" v-if="episodeId">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Create First Shot
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Shots Table -->
|
||||
<div v-else class="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Shot Name</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Frames</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Tasks</TableHead>
|
||||
<TableHead>Updated</TableHead>
|
||||
<TableHead class="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="shot in shots"
|
||||
:key="shot.id"
|
||||
class="hover:bg-muted/50"
|
||||
>
|
||||
<TableCell class="font-medium">{{ shot.name }}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
v-if="shot.description"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
{{ shot.description }}
|
||||
</span>
|
||||
<span v-else class="text-sm text-muted-foreground italic"
|
||||
>No description</span
|
||||
>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span class="font-mono text-sm">
|
||||
{{ shot.frame_start }}-{{ shot.frame_end }}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground ml-2">
|
||||
({{ shot.frame_end - shot.frame_start + 1 }} frames)
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="getStatusVariant(shot.status)">
|
||||
{{ formatStatus(shot.status) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span class="text-sm">{{ shot.task_count }} tasks</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ formatDate(shot.updated_at) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem @click="editShot(shot)">
|
||||
<Edit class="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="viewTasks(shot)">
|
||||
<CheckSquare class="h-4 w-4 mr-2" />
|
||||
View Tasks
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
@click="deleteShot(shot)"
|
||||
class="text-destructive"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import {
|
||||
Camera,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
AlertCircle,
|
||||
MoreHorizontal,
|
||||
Edit,
|
||||
CheckSquare,
|
||||
Trash2,
|
||||
} from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { shotService, type Shot, ShotStatus } from "@/services/shot";
|
||||
|
||||
interface Props {
|
||||
projectId: number;
|
||||
episodeId?: number | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// Reactive state
|
||||
const shots = ref<Shot[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// Methods
|
||||
const loadShots = async () => {
|
||||
if (!props.projectId) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const shotsData = await shotService.getShots(
|
||||
props.projectId,
|
||||
props.episodeId || undefined
|
||||
);
|
||||
shots.value = shotsData;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Failed to load shots";
|
||||
shots.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshShots = () => {
|
||||
loadShots();
|
||||
};
|
||||
|
||||
const createShot = () => {
|
||||
// TODO: Implement shot creation dialog
|
||||
console.log("Create shot for episode:", props.episodeId);
|
||||
};
|
||||
|
||||
const editShot = (shot: Shot) => {
|
||||
// TODO: Implement shot editing
|
||||
console.log("Edit shot:", shot);
|
||||
};
|
||||
|
||||
const viewTasks = (shot: Shot) => {
|
||||
// TODO: Navigate to shot tasks view
|
||||
console.log("View tasks for shot:", shot);
|
||||
};
|
||||
|
||||
const deleteShot = async (shot: Shot) => {
|
||||
// TODO: Implement shot deletion with confirmation
|
||||
console.log("Delete shot:", shot);
|
||||
};
|
||||
|
||||
const getStatusVariant = (status: ShotStatus) => {
|
||||
switch (status) {
|
||||
case ShotStatus.NOT_STARTED:
|
||||
return "secondary";
|
||||
case ShotStatus.IN_PROGRESS:
|
||||
return "default";
|
||||
case ShotStatus.ON_HOLD:
|
||||
return "outline";
|
||||
case ShotStatus.COMPLETED:
|
||||
return "default";
|
||||
case ShotStatus.APPROVED:
|
||||
return "default";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
};
|
||||
|
||||
const formatStatus = (status: ShotStatus) => {
|
||||
return status
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => [props.projectId, props.episodeId],
|
||||
() => {
|
||||
loadShots();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadShots();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user