Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ addopts = [
"--strict-markers",
"--cov=app",
"--cov-report=term-missing",
"--cov-fail-under=75",
"--cov-fail-under=80",
]
filterwarnings = [
"error",
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type {
Category,
Label,
Project,
ProjectCreate,
ProjectListItem,
ProjectType,
} from '../types/project'

interface Profile {
Expand Down Expand Up @@ -110,8 +111,12 @@ export function deleteProject(id: string) {
})
}

export function listCategories() {
return request<Category[]>('/categories/')
export function listProjectTypes() {
return request<ProjectType[]>('/project-types/')
}

export function listLabels() {
return request<Label[]>('/labels/')
}

export function createProject(input: ProjectCreate) {
Expand Down
217 changes: 153 additions & 64 deletions frontend/src/components/BotCard.vue
Original file line number Diff line number Diff line change
@@ -1,85 +1,116 @@
<script setup lang="ts">
import { computed } from 'vue'
import { RouterLink } from 'vue-router'
import { CheckIcon } from '@heroicons/vue/24/outline'

import type { ProjectListItem } from '../types/project'

defineProps<{
const props = defineProps<{
project: ProjectListItem
}>()

const MAX_VISIBLE_LABELS = 2

const visibleLabels = computed(() =>
props.project.labels.slice(0, MAX_VISIBLE_LABELS),
)

const hiddenLabelCount = computed(() =>
Math.max(
props.project.labels.length - MAX_VISIBLE_LABELS,
0,
),
)

const ownerInitial = computed(() =>
(
props.project.owner?.display_name ||
props.project.owner?.matrix_id ||
'?'
)
.replace(/^@/, '')
.charAt(0)
.toUpperCase(),
)
</script>

<template>
<RouterLink
:to="`/bots/${project.id}`"
class="group block rounded-[14px] border border-[var(--border)]
bg-[var(--surface)] p-[18px] no-underline transition
hover:border-[var(--border-strong)] hover:shadow-[var(--shadow)]"
class="
group block rounded-[14px]
border border-[var(--border)]
bg-[var(--surface)]
p-4 no-underline
transition
hover:border-[var(--border-strong)]
hover:shadow-[var(--shadow)]
sm:p-[18px]
"
>
<div class="flex gap-3.5">
<!-- Bot avatar -->
<div class="flex gap-3 sm:gap-3.5">
<!-- Project avatar -->
<div
class="grid size-11 shrink-0 place-items-center
rounded-[11px] bg-[var(--accent-soft)]
font-display text-lg font-semibold
text-[var(--accent-ink)]"
class="
grid size-10 shrink-0 place-items-center
rounded-[10px]
bg-[var(--accent-soft)]
font-display text-base font-semibold
text-[var(--accent-ink)]
sm:size-11 sm:rounded-[11px] sm:text-lg
"
>
{{ project.name.charAt(0).toUpperCase() }}
</div>

<!-- Bot information -->
<!-- Project information -->
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<h2
class="truncate font-display
text-base font-semibold tracking-[-0.01em]
text-[var(--text)]"
>
{{ project.name }}
</h2>

<!-- Placeholder until project verification exists -->
<span
v-if="false"
class="grid size-[15px] shrink-0 place-items-center
rounded-full bg-[var(--accent)]
text-[#0e1012]"
title="Verified"
>
<CheckIcon class="size-2.5" />
</span>
</div>

<p
class="mt-1.5 line-clamp-2 text-[13.5px]
leading-[1.5] text-[var(--muted)]"
>
{{ project.short_description }}
</p>
<!-- Header -->
<div class="flex min-w-0 items-center justify-between gap-3">
<!-- Title -->
<div class="flex min-w-0 items-center gap-2">
<h2
class="
min-w-0 truncate
font-display text-[15px] font-semibold
tracking-[-0.01em]
text-[var(--text)]
sm:text-base
"
>
{{ project.name }}
</h2>

<div class="mt-3 flex items-end justify-between gap-3">
<!-- Categories -->
<div class="flex min-w-0 flex-wrap gap-1.5">
<!-- Placeholder until project verification exists -->
<span
v-for="category in project.categories"
:key="category.id"
class="rounded-[6px] bg-[var(--sunk)]
px-2 py-[3px] text-[11.5px]
text-[var(--muted)]"
v-if="false"
class="
grid size-[15px] shrink-0 place-items-center
rounded-full bg-[var(--accent)]
text-[#0e1012]
"
title="Verified"
>
{{ category.name }}
<CheckIcon class="size-2.5" />
</span>
</div>

<!-- Owner -->
<div
v-if="project.owner"
class="hidden shrink-0 items-center gap-1.5 lg:flex"
class="
hidden min-w-0 shrink items-center gap-1.5
lg:flex
"
>
<div
class="grid size-5 place-items-center overflow-hidden
rounded-full bg-[var(--accent-soft)]
text-[9px] font-semibold text-[var(--accent-ink)]"
class="
grid size-5 shrink-0 place-items-center
overflow-hidden rounded-full
bg-[var(--accent-soft)]
text-[9px] font-semibold
text-[var(--accent-ink)]
"
>
<img
v-if="project.owner.avatar_url"
Expand All @@ -89,22 +120,17 @@ defineProps<{
>

<span v-else>
{{
(
project.owner.display_name ||
project.owner.matrix_id ||
'?'
)
.replace(/^@/, '')
.charAt(0)
.toUpperCase()
}}
{{ ownerInitial }}
</span>
</div>

<span
class="max-w-[100px] truncate font-mono
text-[10.5px] text-[var(--faint)]"
class="
max-w-[90px] truncate
font-mono text-[10px]
text-[var(--faint)]
xl:max-w-[120px]
"
>
{{
project.owner.display_name ||
Expand All @@ -114,7 +140,70 @@ defineProps<{
</span>
</div>
</div>

<!-- Description -->
<p
class="
mt-1.5 line-clamp-2
text-[13px] leading-[1.5]
text-[var(--muted)]
sm:text-[13.5px]
"
>
{{ project.short_description }}
</p>

<!-- Metadata -->
<div class="mt-3 flex min-w-0 flex-wrap items-center gap-1.5">
<!-- Project type -->
<span
class="
shrink-0 rounded-[6px]
bg-[var(--accent-soft)]
px-2 py-[3px]
text-[11px] font-medium
text-[var(--accent-ink)]
sm:text-[11.5px]
"
>
{{ project.project_type.name }}
</span>

<!-- Primary label -->
<span
v-for="label in visibleLabels"
:key="label.id"
class="
shrink-0 rounded-[6px]
bg-[var(--sunk)]
px-2 py-[3px]
text-[11px] text-[var(--muted)]
sm:text-[11.5px]
"
>
{{ label.name }}
</span>

<!-- Remaining labels -->
<span
v-if="hiddenLabelCount"
class="
shrink-0 rounded-[6px]
bg-[var(--sunk)]
px-2 py-[3px]
font-mono text-[10px]
text-[var(--faint)]
"
:title="project.labels
.slice(MAX_VISIBLE_LABELS)
.map((label) => label.name)
.join(', ')
"
>
+{{ hiddenLabelCount }}
</span>
</div>
</div>
</div>
</RouterLink>
</template>
</template>
Loading