Implement projects and project keys

This commit is contained in:
2026-03-25 00:44:37 +01:00
parent aeee3d2640
commit 7d4b0733b3
16 changed files with 716 additions and 1 deletions
+13 -1
View File
@@ -1,6 +1,12 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { BookOpen, FolderGit2, LayoutGrid, Shield } from 'lucide-vue-next';
import {
BookOpen,
Folder,
FolderGit2,
LayoutGrid,
Shield,
} from 'lucide-vue-next';
import { computed } from 'vue';
import AppLogo from '@/components/AppLogo.vue';
import NavFooter from '@/components/NavFooter.vue';
@@ -16,6 +22,7 @@ import {
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { index as adminUsers } from '@/routes/admin/users';
import { index as projectsIndex } from '@/routes/projects';
import { dashboard } from '@/routes';
import type { NavItem } from '@/types';
@@ -28,6 +35,11 @@ const mainNavItems = computed<NavItem[]>(() => {
href: dashboard(),
icon: LayoutGrid,
},
{
title: 'Projects',
href: projectsIndex(),
icon: Folder,
},
];
if (page.props.auth.user.is_admin) {
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import ProjectController from '@/actions/App/Http/Controllers/ProjectController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { create, index } from '@/routes/projects';
import type { BreadcrumbItem } from '@/types';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Projects',
href: index(),
},
{
title: 'New project',
href: create(),
},
];
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="New project" />
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
<Heading
title="Create project"
description="Create a project with a stable uppercase key that will prefix all future ticket keys."
/>
<Card>
<CardHeader>
<CardTitle>Project details</CardTitle>
<CardDescription>
The key is uppercased automatically, must be unique, and
can contain up to 5 characters.
</CardDescription>
</CardHeader>
<CardContent>
<Form
v-bind="ProjectController.store.form()"
class="space-y-6"
v-slot="{ errors, processing }"
>
<div class="grid gap-2">
<Label for="name">Project name</Label>
<Input
id="name"
name="name"
required
autofocus
placeholder="Kanban foundation"
/>
<InputError :message="errors.name" />
</div>
<div class="grid gap-2">
<Label for="key">Project key</Label>
<Input
id="key"
name="key"
required
maxlength="5"
placeholder="CORE"
class="uppercase"
/>
<p class="text-sm text-muted-foreground">
Example ticket keys will look like
<span class="font-medium text-foreground"
>CORE-1</span
>.
</p>
<InputError :message="errors.key" />
</div>
<div class="flex items-center gap-3">
<Button type="submit" :disabled="processing">
Create project
</Button>
<Button as-child variant="outline">
<Link :href="index()">Cancel</Link>
</Button>
</div>
</Form>
</CardContent>
</Card>
</div>
</AppLayout>
</template>
+93
View File
@@ -0,0 +1,93 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { Folder, Plus } from 'lucide-vue-next';
import Heading from '@/components/Heading.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { create, index, show } from '@/routes/projects';
import type { BreadcrumbItem, ProjectsIndexPageProps } from '@/types';
defineProps<ProjectsIndexPageProps>();
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Projects',
href: index(),
},
];
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Projects" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<Heading
title="Projects"
description="Create and browse project spaces that will later host boards, backlog, and settings."
/>
<Button as-child>
<Link :href="create()">
<Plus class="size-4" />
New project
</Link>
</Button>
</div>
<div
v-if="projects.length === 0"
class="rounded-xl border border-dashed px-6 py-10 text-center text-sm text-muted-foreground"
>
No projects exist yet. Create the first one to start building
out board, backlog, and settings areas.
</div>
<div v-else class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<Link
v-for="project in projects"
:key="project.id"
:href="show(project.id)"
class="group block"
>
<Card
class="h-full border-border/80 transition-colors group-hover:border-primary/40"
>
<CardHeader class="gap-3">
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<CardTitle>{{ project.name }}</CardTitle>
<CardDescription>
Project workspace entry point
</CardDescription>
</div>
<Badge variant="outline" class="px-2.5 py-1">
{{ project.key }}
</Badge>
</div>
</CardHeader>
<CardContent
class="flex items-center gap-2 text-sm text-muted-foreground"
>
<Folder class="size-4" />
Open board, backlog, and settings hub
</CardContent>
</Card>
</Link>
</div>
</div>
</AppLayout>
</template>
+100
View File
@@ -0,0 +1,100 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { LayoutGrid, ListTodo, Settings2 } from 'lucide-vue-next';
import Heading from '@/components/Heading.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { index, show } from '@/routes/projects';
import type { BreadcrumbItem, ProjectShowPageProps } from '@/types';
const props = defineProps<ProjectShowPageProps>();
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Projects',
href: index(),
},
{
title: props.project.name,
href: show(props.project.id),
},
];
const projectAreas = [
{
title: 'Backlog',
description:
'This area will receive new tickets first and act as the intake queue for project work.',
icon: ListTodo,
},
{
title: 'Board',
description:
'This area will become the configurable Kanban board with project-specific columns.',
icon: LayoutGrid,
},
{
title: 'Project settings',
description:
'This area will host project-level configuration such as ticket metadata and workflow options.',
icon: Settings2,
},
];
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head :title="project.name" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-3">
<Badge variant="outline" class="px-3 py-1">
{{ project.key }}
</Badge>
<Heading
:title="project.name"
description="Project hub for the upcoming backlog, board, and settings work."
/>
</div>
<Button as-child variant="outline">
<Link :href="index()">Back to projects</Link>
</Button>
</div>
<div class="grid gap-4 md:grid-cols-3">
<Card
v-for="area in projectAreas"
:key="area.title"
class="h-full"
>
<CardHeader>
<div
class="mb-3 flex size-10 items-center justify-center rounded-full bg-muted text-foreground"
>
<component :is="area.icon" class="size-5" />
</div>
<CardTitle>{{ area.title }}</CardTitle>
<CardDescription>{{
area.description
}}</CardDescription>
</CardHeader>
<CardContent class="text-sm text-muted-foreground">
Coming next in the project workflow implementation.
</CardContent>
</Card>
</div>
</div>
</AppLayout>
</template>
@@ -0,0 +1,8 @@
export type ProjectModel = {
id: number;
name: string;
key: string;
created_at: string;
updated_at: string;
[key: string]: unknown;
};
+9
View File
@@ -1,6 +1,15 @@
import type { ProjectModel } from '@/types/models/project-model';
import type { UserModel } from '@/types/models/user-model';
export type AdminUsersPageProps = {
users: UserModel[];
status?: string | null;
};
export type ProjectsIndexPageProps = {
projects: ProjectModel[];
};
export type ProjectShowPageProps = {
project: ProjectModel;
};