feat(KAN-23): rebuild project overview and entry pages
This commit is contained in:
@@ -1,4 +1,41 @@
|
||||
export const en = {
|
||||
'projects.workspace': 'Workspace',
|
||||
'projects.description':
|
||||
'Your project spaces, ready to work in. Open a project or jump straight to its board and backlog.',
|
||||
'projects.create': 'New project',
|
||||
'projects.updated': 'Project update',
|
||||
'projects.available': {
|
||||
one: '{count} project available',
|
||||
other: '{count} projects available',
|
||||
},
|
||||
'projects.open': 'Open {name} overview',
|
||||
'projects.openBoard': 'Open {name} board',
|
||||
'projects.openBacklog': 'Open {name} backlog',
|
||||
'projects.overview': 'Project overview',
|
||||
'projects.empty.title': 'No projects to show',
|
||||
'projects.empty.createDescription':
|
||||
'Create a project to give your team a place to plan and track work.',
|
||||
'projects.empty.accessDescription':
|
||||
'Projects appear here when you have access. Ask a workspace or project administrator to add you.',
|
||||
'projects.empty.create': 'Create your first project',
|
||||
'projects.back': 'All projects',
|
||||
'projects.hubDescription':
|
||||
'Follow work on the board or explore tickets in the backlog.',
|
||||
'projects.workAreas': 'Project work areas',
|
||||
'projects.boardDescription':
|
||||
'See work across the project’s workflow and open tickets in their current status.',
|
||||
'projects.backlogDescription':
|
||||
'Explore incoming work, review ticket details, and plan what comes next.',
|
||||
'projects.boardAction': 'Open board',
|
||||
'projects.backlogAction': 'Open backlog',
|
||||
'projects.settingsDescription':
|
||||
'Manage this project’s configuration, integrations, and access.',
|
||||
'projects.saveTemplate': 'Save as new template',
|
||||
'projects.templateTitle': 'Based on {name}',
|
||||
'projects.templateDescription':
|
||||
'Inherited areas follow the linked template. Overridden areas use this project’s own configuration.',
|
||||
'projects.inheritedArea': '{name} · inherited',
|
||||
'projects.overriddenArea': '{name} · overridden',
|
||||
'auth.home': 'KanbanGit home',
|
||||
'auth.accountAccess': 'Account access',
|
||||
'auth.backToLogin': 'Return to sign in',
|
||||
|
||||
@@ -1,103 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Folder, Plus } from 'lucide-vue-next';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Columns3,
|
||||
FolderGit2,
|
||||
ListTodo,
|
||||
Plus,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import FeedbackState from '@/components/FeedbackState.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { create, index, show } from '@/routes/projects/index';
|
||||
import { create, index, show } from '@/routes/projects';
|
||||
import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import type { BreadcrumbItem, ProjectsIndexPageProps } from '@/types';
|
||||
|
||||
defineProps<ProjectsIndexPageProps>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Projects',
|
||||
href: index(),
|
||||
},
|
||||
];
|
||||
const { t } = useLocalization();
|
||||
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
||||
{ title: t('navigation.projects'), href: index() },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<Head title="Projects" />
|
||||
<Head :title="t('navigation.projects')" />
|
||||
|
||||
<div class="flex flex-1 flex-col gap-6 p-4">
|
||||
<div
|
||||
<div class="gap-page md:p-page flex min-w-0 flex-1 flex-col p-4">
|
||||
<header
|
||||
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."
|
||||
/>
|
||||
|
||||
<div class="gap-stack grid min-w-0">
|
||||
<p class="text-label text-muted-foreground uppercase">
|
||||
{{ t('projects.workspace') }}
|
||||
</p>
|
||||
<h1 class="text-headline-mobile md:text-headline">
|
||||
{{ t('navigation.projects') }}
|
||||
</h1>
|
||||
<p class="text-muted-foreground max-w-2xl">
|
||||
{{ t('projects.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="$page.props.auth.permissions.projectCreate"
|
||||
as-child
|
||||
class="self-start"
|
||||
>
|
||||
<Link :href="create()">
|
||||
<Plus class="size-4" />
|
||||
New project
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status"
|
||||
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="projects.length === 0"
|
||||
class="text-muted-foreground rounded-xl border border-dashed px-6 py-10 text-center text-sm"
|
||||
>
|
||||
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="border-border/80 group-hover:border-primary/40 h-full transition-colors"
|
||||
<Link :href="create()"
|
||||
><Plus aria-hidden="true" />{{
|
||||
t('projects.create')
|
||||
}}</Link
|
||||
>
|
||||
<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>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<Badge variant="outline" class="px-2.5 py-1">
|
||||
{{ project.key }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<FeedbackState
|
||||
v-if="status"
|
||||
state="success"
|
||||
:title="t('projects.updated')"
|
||||
:description="status"
|
||||
/>
|
||||
|
||||
<CardContent
|
||||
class="text-muted-foreground flex items-center gap-2 text-sm"
|
||||
<FeedbackState
|
||||
v-if="projects.length === 0"
|
||||
state="empty"
|
||||
:title="t('projects.empty.title')"
|
||||
:description="
|
||||
$page.props.auth.permissions.projectCreate
|
||||
? t('projects.empty.createDescription')
|
||||
: t('projects.empty.accessDescription')
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-if="$page.props.auth.permissions.projectCreate"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
<Link :href="create()"
|
||||
><Plus aria-hidden="true" />{{
|
||||
t('projects.empty.create')
|
||||
}}</Link
|
||||
>
|
||||
</Button>
|
||||
</FeedbackState>
|
||||
|
||||
<section
|
||||
v-else
|
||||
aria-labelledby="project-list-title"
|
||||
class="gap-column grid"
|
||||
>
|
||||
<h2
|
||||
id="project-list-title"
|
||||
class="text-caption text-muted-foreground"
|
||||
>
|
||||
{{ t('projects.available', { count: projects.length }) }}
|
||||
</h2>
|
||||
<ul class="gap-column grid md:grid-cols-2 xl:grid-cols-3">
|
||||
<li
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
class="min-w-0"
|
||||
>
|
||||
<article
|
||||
class="bg-card text-card-foreground flex h-full min-w-0 flex-col rounded-md border"
|
||||
>
|
||||
<Folder class="size-4" />
|
||||
Open board, backlog, and settings hub
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
:href="show(project.id)"
|
||||
:aria-label="
|
||||
t('projects.open', { name: project.name })
|
||||
"
|
||||
class="gap-column hover:bg-accent/50 group flex min-w-0 flex-1 items-start rounded-t-md p-5 transition-colors"
|
||||
>
|
||||
<span
|
||||
class="bg-accent text-primary flex size-11 shrink-0 items-center justify-center rounded-md"
|
||||
>
|
||||
<FolderGit2
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<div class="gap-stack grid min-w-0 flex-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="max-w-full justify-self-start whitespace-normal break-all"
|
||||
>{{ project.key }}</Badge
|
||||
>
|
||||
<h3
|
||||
class="text-title group-hover:text-primary break-words"
|
||||
>
|
||||
{{ project.name }}
|
||||
</h3>
|
||||
<span
|
||||
class="text-caption text-muted-foreground"
|
||||
>{{ t('projects.overview') }}</span
|
||||
>
|
||||
</div>
|
||||
<ArrowUpRight
|
||||
class="text-muted-foreground mt-1 size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
<div
|
||||
class="gap-stack p-card grid grid-cols-2 border-t"
|
||||
>
|
||||
<Button as-child variant="secondary">
|
||||
<Link
|
||||
:href="boardIndex(project.id)"
|
||||
:aria-label="
|
||||
t('projects.openBoard', {
|
||||
name: project.name,
|
||||
})
|
||||
"
|
||||
>
|
||||
<Columns3 aria-hidden="true" />{{
|
||||
t('navigation.board')
|
||||
}}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button as-child variant="ghost">
|
||||
<Link
|
||||
:href="backlogIndex(project.id)"
|
||||
:aria-label="
|
||||
t('projects.openBacklog', {
|
||||
name: project.name,
|
||||
})
|
||||
"
|
||||
>
|
||||
<ListTodo aria-hidden="true" />{{
|
||||
t('navigation.backlog')
|
||||
}}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,206 +1,227 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { FolderGit2, LayoutGrid, ListTodo, Settings2 } from 'lucide-vue-next';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Columns3,
|
||||
Layers3,
|
||||
ListTodo,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { access, index, show } from '@/routes/projects/index';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import { projectSettingsNavigation } from '@/lib/navigation';
|
||||
import { index, show } from '@/routes/projects';
|
||||
import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||
import {
|
||||
board as boardSettings,
|
||||
general as generalSettings,
|
||||
metadata as metadataSettings,
|
||||
notifications as notificationSettings,
|
||||
repositories as repositorySettings,
|
||||
} from '@/routes/projects/settings';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import { create as projectTemplateCreate } from '@/routes/projects/templates';
|
||||
import type { BreadcrumbItem, ProjectShowPageProps } from '@/types';
|
||||
|
||||
const props = defineProps<ProjectShowPageProps>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
const { t } = useLocalization();
|
||||
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
||||
{ title: t('navigation.projects'), href: index() },
|
||||
{ title: props.project.name, href: show(props.project.id) },
|
||||
]);
|
||||
const projectAreas = computed(() => [
|
||||
{
|
||||
title: 'Projects',
|
||||
href: index(),
|
||||
title: t('navigation.board'),
|
||||
description: t('projects.boardDescription'),
|
||||
action: t('projects.boardAction'),
|
||||
href: boardIndex(props.project.id),
|
||||
icon: Columns3,
|
||||
},
|
||||
{
|
||||
title: props.project.name,
|
||||
href: show(props.project.id),
|
||||
},
|
||||
];
|
||||
|
||||
const projectAreas = [
|
||||
{
|
||||
title: 'Backlog',
|
||||
description:
|
||||
'Create tickets, review incoming work, and move ready items into the board workflow.',
|
||||
title: t('navigation.backlog'),
|
||||
description: t('projects.backlogDescription'),
|
||||
action: t('projects.backlogAction'),
|
||||
href: backlogIndex(props.project.id),
|
||||
icon: ListTodo,
|
||||
},
|
||||
{
|
||||
title: 'Board',
|
||||
description:
|
||||
'Track active work across the project-defined Kanban columns and update ticket status.',
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
title: 'Project settings',
|
||||
description:
|
||||
'Manage board columns, ticket metadata, and the project-specific workflow configuration.',
|
||||
icon: Settings2,
|
||||
},
|
||||
{
|
||||
title: 'Repositories',
|
||||
description:
|
||||
'Link project repositories so tickets can later discover related branches and pull requests.',
|
||||
icon: FolderGit2,
|
||||
},
|
||||
];
|
||||
]);
|
||||
const settingsItems = computed(() =>
|
||||
projectSettingsNavigation(
|
||||
{
|
||||
id: props.project.id,
|
||||
name: props.project.name,
|
||||
key: props.project.key,
|
||||
canManage: props.canManageAccess,
|
||||
canCreateTickets: false,
|
||||
},
|
||||
t,
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<Head :title="project.name" />
|
||||
|
||||
<div class="flex flex-1 flex-col gap-6 p-4">
|
||||
<div
|
||||
<div class="gap-page md:p-page flex min-w-0 flex-1 flex-col p-4">
|
||||
<Link
|
||||
:href="index()"
|
||||
class="text-caption text-muted-foreground hover:text-primary min-h-control inline-flex items-center gap-2 self-start rounded-md"
|
||||
>
|
||||
<ArrowLeft class="size-4" aria-hidden="true" />{{
|
||||
t('projects.back')
|
||||
}}
|
||||
</Link>
|
||||
|
||||
<header
|
||||
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>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="boardIndex(project.id)">Open board</Link>
|
||||
</Button>
|
||||
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="backlogIndex(project.id)">
|
||||
Open backlog
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="$page.props.auth.permissions.projectCreate"
|
||||
as-child
|
||||
<div class="gap-stack grid min-w-0">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="max-w-full justify-self-start whitespace-normal break-all"
|
||||
>{{ project.key }}</Badge
|
||||
>
|
||||
<Link :href="projectTemplateCreate(project.id)">
|
||||
Save as new template
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="index()">Back to projects</Link>
|
||||
</Button>
|
||||
<h1
|
||||
class="text-headline-mobile md:text-headline break-words"
|
||||
>
|
||||
{{ project.name }}
|
||||
</h1>
|
||||
<p class="text-muted-foreground max-w-2xl">
|
||||
{{ t('projects.hubDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="$page.props.auth.permissions.projectCreate"
|
||||
as-child
|
||||
variant="outline"
|
||||
class="self-start"
|
||||
>
|
||||
<Link :href="projectTemplateCreate(project.id)"
|
||||
><Layers3 aria-hidden="true" />{{
|
||||
t('projects.saveTemplate')
|
||||
}}</Link
|
||||
>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<Card v-if="templateState?.inheritsFromTemplate">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Built from template {{ templateState.templateName }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
This project keeps its template link. Inherited areas
|
||||
follow the template baseline; overridden areas were
|
||||
customized locally.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-wrap gap-2">
|
||||
<section
|
||||
:aria-label="t('projects.workAreas')"
|
||||
class="gap-column grid md:grid-cols-2"
|
||||
>
|
||||
<Link
|
||||
v-for="area in projectAreas"
|
||||
:key="area.href.url"
|
||||
:href="area.href"
|
||||
:aria-label="area.action"
|
||||
class="gap-column bg-card hover:border-primary/50 hover:shadow-card-hover group flex min-w-0 flex-col rounded-md border p-5 transition-[border-color,box-shadow]"
|
||||
>
|
||||
<span
|
||||
class="bg-accent text-primary flex size-11 items-center justify-center rounded-md"
|
||||
>
|
||||
<component
|
||||
:is="area.icon"
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<div class="gap-stack grid">
|
||||
<h2 class="text-title">{{ area.title }}</h2>
|
||||
<p class="text-muted-foreground">
|
||||
{{ area.description }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="text-primary mt-auto flex items-center gap-2 pt-2 font-semibold"
|
||||
>
|
||||
{{ area.action
|
||||
}}<ArrowRight class="size-4" aria-hidden="true" />
|
||||
</span>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="settingsItems.length"
|
||||
aria-labelledby="project-settings-title"
|
||||
class="gap-column grid"
|
||||
>
|
||||
<div class="gap-stack grid">
|
||||
<h2 id="project-settings-title" class="text-title">
|
||||
{{ t('navigation.projectSettings') }}
|
||||
</h2>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('projects.settingsDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="gap-stack grid sm:grid-cols-2 xl:grid-cols-3">
|
||||
<li
|
||||
v-for="item in settingsItems"
|
||||
:key="item.title"
|
||||
class="min-w-0"
|
||||
>
|
||||
<Link
|
||||
:href="item.href"
|
||||
class="gap-card bg-card hover:bg-accent hover:text-accent-foreground min-h-control flex h-full items-center rounded-md border p-4 transition-colors"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
class="text-muted-foreground size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
class="min-w-0 flex-1 break-words font-medium"
|
||||
>{{ item.title }}</span
|
||||
>
|
||||
<ArrowRight
|
||||
class="text-muted-foreground size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="templateState?.inheritsFromTemplate"
|
||||
aria-labelledby="project-template-title"
|
||||
class="gap-column bg-card flex min-w-0 flex-col rounded-md border p-5"
|
||||
>
|
||||
<div class="gap-card flex items-start">
|
||||
<Layers3
|
||||
class="text-muted-foreground mt-0.5 size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="gap-stack grid min-w-0">
|
||||
<h2
|
||||
id="project-template-title"
|
||||
class="text-title break-words"
|
||||
>
|
||||
{{
|
||||
t('projects.templateTitle', {
|
||||
name: templateState.templateName,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('projects.templateDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-stack flex flex-wrap">
|
||||
<Badge
|
||||
v-for="area in templateState.inheritedAreas"
|
||||
:key="`inherited-${area.value}`"
|
||||
variant="outline"
|
||||
class="max-w-full whitespace-normal break-words"
|
||||
>
|
||||
{{ area.label }} · inherited
|
||||
{{ t('projects.inheritedArea', { name: area.label }) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-for="area in templateState.overriddenAreas"
|
||||
:key="`overridden-${area.value}`"
|
||||
variant="secondary"
|
||||
class="max-w-full whitespace-normal break-words"
|
||||
>
|
||||
{{ area.label }} · overridden
|
||||
{{ t('projects.overriddenArea', { name: area.label }) }}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div v-if="canManageAccess" class="flex flex-wrap gap-3">
|
||||
<Button as-child>
|
||||
<Link :href="boardSettings(project.id)">
|
||||
Board settings
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="$page.props.auth.permissions.globalAdmin"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
<Link :href="generalSettings(project.id)">
|
||||
General settings
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="metadataSettings(project.id)">
|
||||
Ticket settings
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="repositorySettings(project.id)">
|
||||
Repository settings
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="notificationSettings(project.id)">
|
||||
Notification settings
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button as-child variant="outline">
|
||||
<Link :href="access(project.id)">Manage access</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="bg-muted text-foreground mb-3 flex size-10 items-center justify-center rounded-full"
|
||||
>
|
||||
<component :is="area.icon" class="size-5" />
|
||||
</div>
|
||||
<CardTitle>{{ area.title }}</CardTitle>
|
||||
<CardDescription>{{
|
||||
area.description
|
||||
}}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="text-muted-foreground text-sm">
|
||||
Use the project shortcuts above to open this area.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user