Skip to content

Commit a411216

Browse files
authored
feat(editor): handle custom inputs based on types (#214)
1 parent 230e31e commit a411216

37 files changed

+1170
-87
lines changed
Lines changed: 123 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,132 @@
11
<script setup lang="ts">
22
const { data: page } = await useAsyncData('authors-page', () => queryCollection('pages').first())
33
4-
const { data: authors } = await useAsyncData('authors-list', () => queryCollection('authors').all())
4+
const { data: authors } = await useAsyncData('authors-list', () => queryCollection('authors').order('order', 'ASC').all())
5+
6+
const roleConfig: Record<string, { color: 'warning' | 'info' | 'success', icon: string }> = {
7+
creator: { color: 'warning', icon: 'i-lucide-crown' },
8+
maintainer: { color: 'info', icon: 'i-lucide-shield-check' },
9+
contributor: { color: 'success', icon: 'i-lucide-git-pull-request' },
10+
}
11+
12+
function formatDate(date: string | Date | undefined): string {
13+
if (!date) return ''
14+
const d = new Date(date)
15+
return d.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
16+
}
517
</script>
618

719
<template>
8-
<div>
9-
<UContainer>
10-
<ContentRenderer :value="page" />
11-
<div class="flex flex-col gap-2">
12-
<UUser
13-
v-for="author in authors"
14-
:key="author.name"
15-
:name="author.name"
16-
:description="author.username ? `@${author.username}` : ''"
17-
:avatar="author.avatar"
18-
:to="author.to"
20+
<UContainer>
21+
<ContentRenderer
22+
v-if="page"
23+
:value="page"
24+
/>
25+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
26+
<UCard
27+
v-for="author in authors"
28+
:key="author.name"
29+
class="group hover:ring-2 hover:ring-primary/50 transition-all duration-200 relative overflow-visible"
30+
>
31+
<UBadge
32+
v-if="author.isOpenSourceLover"
33+
color="primary"
34+
size="sm"
35+
class="absolute -top-2 -right-2 z-10"
36+
title="Open Source Lover"
1937
>
20-
<template #name>
21-
<div class="flex gap-2">
22-
<span class="text-sm font-medium">{{ author.name }}</span>
23-
<span class="text-sm text-muted">
24-
- {{ author.username ? `@${author.username}` : '' }}
25-
</span>
26-
</div>
27-
</template>
28-
29-
<template #description>
30-
<div class="flex gap-2">
31-
<UBadge
32-
v-for="module in author.modules"
33-
:key="module"
34-
variant="soft"
35-
size="xs"
36-
color="neutral"
37-
>
38-
{{ module }}
39-
</UBadge>
38+
<UIcon
39+
name="i-lucide-heart"
40+
class="size-3"
41+
/>
42+
</UBadge>
43+
44+
<div class="flex flex-col items-center text-center gap-4">
45+
<div class="relative">
46+
<UAvatar
47+
:src="author.avatar?.src"
48+
:alt="author.avatar?.alt || author.name"
49+
size="3xl"
50+
class="ring-4 ring-muted group-hover:ring-primary/30 transition-all duration-200"
51+
/>
52+
<div
53+
v-if="author.icon"
54+
class="absolute -bottom-2 -left-2 size-7 flex items-center justify-center bg-elevated rounded-full ring-2 ring-default"
55+
>
56+
<UIcon
57+
:name="author.icon"
58+
class="size-4 text-primary"
59+
/>
4060
</div>
41-
</template>
42-
</UUser>
43-
</div>
44-
</UContainer>
45-
</div>
61+
</div>
62+
63+
<div class="flex flex-col items-center gap-2">
64+
<NuxtLink
65+
:to="author.to"
66+
target="_blank"
67+
class="text-lg font-semibold text-highlighted hover:text-primary transition-colors"
68+
>
69+
{{ author.name }}
70+
</NuxtLink>
71+
<span
72+
v-if="author.username"
73+
class="text-sm text-muted"
74+
>
75+
@{{ author.username }}
76+
</span>
77+
<UBadge
78+
v-if="author.role"
79+
:color="roleConfig[author.role]?.color || 'neutral'"
80+
variant="subtle"
81+
size="xs"
82+
class="capitalize"
83+
>
84+
<UIcon
85+
:name="roleConfig[author.role]?.icon || 'i-lucide-user'"
86+
class="size-3 mr-1"
87+
/>
88+
{{ author.role }}
89+
</UBadge>
90+
<span
91+
v-if="author.birthDate"
92+
class="text-xs text-dimmed flex items-center gap-1"
93+
>
94+
<UIcon
95+
name="i-lucide-cake"
96+
class="size-3"
97+
/>
98+
{{ formatDate(author.birthDate) }}
99+
</span>
100+
</div>
101+
102+
<div
103+
v-if="author.modules?.length"
104+
class="flex flex-wrap justify-center gap-1.5"
105+
>
106+
<UBadge
107+
v-for="module in author.modules"
108+
:key="module"
109+
variant="outline"
110+
size="sm"
111+
color="neutral"
112+
>
113+
{{ module }}
114+
</UBadge>
115+
</div>
116+
117+
<UButton
118+
:to="author.to"
119+
target="_blank"
120+
variant="subtle"
121+
color="neutral"
122+
size="sm"
123+
trailing-icon="i-lucide-external-link"
124+
class="mt-2"
125+
>
126+
View Profile
127+
</UButton>
128+
</div>
129+
</UCard>
130+
</div>
131+
</UContainer>
46132
</template>

playground/docus/content.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ const createDocsSchema = () => z.object({
1414
const createAuthorsSchema = () => z.object({
1515
name: z.string(),
1616
avatar: z.object({
17-
src: z.string(),
17+
src: z.string().editor({ input: 'media' }),
1818
alt: z.string(),
1919
}),
2020
to: z.string(),
2121
username: z.string(),
22+
role: z.enum(['creator', 'maintainer', 'contributor']).default('contributor'),
23+
order: z.number().default(0),
24+
birthDate: z.date(),
25+
icon: z.string().editor({ input: 'icon', iconLibraries: ['lucide'] }),
26+
isOpenSourceLover: z.boolean().default(true),
2227
modules: z.array(z.string()),
2328
})
2429

playground/docus/content/authors/atinux.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
name: Sébastien Chopin
22
avatar:
3-
src: https://avatars.githubusercontent.com/u/904724?v=4
3+
src: /atinux.jpeg
44
to: https://x.com/atinux
55
username: atinux
6+
role: creator
7+
order: 1
8+
birthDate: 1992-11-23
9+
icon: i-lucide-rocket
10+
isOpenSourceLover: true
611
modules:
712
- hub
813
- ui

playground/docus/content/authors/farnabaz.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
---
22
name: Ahad Birang
33
avatar:
4-
src: https://avatars.githubusercontent.com/u/2047945?v=4
4+
src: /farnabaz.jpeg
55
to: https://x.com/farnabaz
66
username: farnabaz
7+
role: maintainer
8+
order: 2
9+
birthDate: 1990-08-10
10+
icon: i-lucide-code
11+
isOpenSourceLover: true
712
modules:
813
- studio
914
- content

playground/docus/content/authors/larbish.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
{
22
"name": "Baptiste Leproux",
33
"avatar": {
4-
"src": "https://avatars.githubusercontent.com/u/7290030?v=4"
4+
"src": "/larbish.jpeg"
55
},
66
"to": "https://x.com/_larbish",
77
"username": "larbish",
8+
"role": "contributor",
9+
"order": 3,
10+
"birthDate": "1993-09-03",
11+
"icon": "i-lucide-palette",
12+
"isOpenSourceLover": true,
813
"modules": [
914
"studio",
1015
"content",
18.2 KB
Loading
35.4 KB
Loading
22.1 KB
Loading
Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
<script setup lang="ts">
22
import { titleCase } from 'scule'
3-
import type { FormItem, FormTree } from '../../types'
4-
import type { PropType } from 'vue'
3+
import type { FormItem, FormTree, FormInputsTypes } from '../../types'
4+
import type { Component, PropType } from 'vue'
55
import { computed, ref, watch } from 'vue'
66
import { applyValueById } from '../../utils/form'
7+
import FormInputArray from './input/FormInputArray.vue'
8+
import InputBoolean from './input/InputBoolean.vue'
9+
import InputDate from './input/InputDate.vue'
10+
import InputIcon from './input/InputIcon.vue'
11+
import InputMedia from './input/InputMedia.vue'
12+
import InputNumber from './input/InputNumber.vue'
13+
import InputText from './input/InputText.vue'
714
815
const props = defineProps({
916
formItem: {
@@ -16,27 +23,21 @@ const form = defineModel({ type: Object as PropType<FormTree>, default: () => ({
1623
1724
const label = computed(() => titleCase(props.formItem.title))
1825
19-
const placeholder = computed(() => {
20-
switch (props.formItem.type) {
21-
case 'string':
22-
return `Enter ${props.formItem.title.toLowerCase()}...`
23-
case 'number':
24-
return '0'
25-
default:
26-
return ''
27-
}
28-
})
26+
const typeComponentMap: Partial<Record<FormInputsTypes, Component>> = {
27+
array: FormInputArray,
28+
boolean: InputBoolean,
29+
date: InputDate,
30+
icon: InputIcon,
31+
media: InputMedia,
32+
number: InputNumber,
33+
string: InputText,
34+
}
2935
30-
const inputType = computed(() => {
31-
switch (props.formItem.type) {
32-
case 'number':
33-
return 'number'
34-
default:
35-
return 'text'
36-
}
37-
})
36+
const inputComponentName = computed(() => typeComponentMap[props.formItem.type] ?? InputText)
3837
39-
const isArrayType = computed(() => props.formItem.type === 'array')
38+
const inputFormItem = computed(() => {
39+
return props.formItem.type === 'array' ? props.formItem.arrayItemForm : props.formItem
40+
})
4041
4142
// Initialize model value
4243
const model = ref(computeValue(props.formItem))
@@ -56,27 +57,25 @@ watch(() => props.formItem, (newFormItem) => {
5657
}, { deep: true })
5758
5859
function computeValue(formItem: FormItem): unknown {
59-
if (formItem.value !== undefined) {
60-
return formItem.value
61-
}
60+
const value = formItem.value
6261
6362
switch (formItem.type) {
6463
case 'string':
6564
case 'date':
6665
case 'icon':
6766
case 'media':
6867
case 'file':
69-
return ''
68+
return typeof value === 'string' ? value : ''
7069
case 'boolean':
71-
return false
70+
return typeof value === 'boolean' ? value : false
7271
case 'number':
73-
return 0
72+
return typeof value === 'number' ? value : 0
7473
case 'array':
75-
return []
74+
return Array.isArray(value) ? value : []
7675
case 'object':
77-
return {}
76+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
7877
default:
79-
return null
78+
return value ?? null
8079
}
8180
}
8281
</script>
@@ -87,22 +86,13 @@ function computeValue(formItem: FormItem): unknown {
8786
:label="label"
8887
:ui="{
8988
root: 'w-full mt-2',
90-
label: 'text-xs font-medium tracking-tight',
89+
label: 'text-xs font-semibold tracking-tight',
9190
}"
9291
>
93-
<FormInputArray
94-
v-if="isArrayType"
95-
v-model="(model as unknown[])"
96-
:form-item="formItem.arrayItemForm"
97-
/>
98-
<UInput
99-
v-else
100-
:id="formItem.id"
101-
v-model="(model as string | number)"
102-
:placeholder="placeholder"
103-
:type="inputType"
104-
size="xs"
105-
class="w-full"
92+
<component
93+
:is="inputComponentName"
94+
v-model="model"
95+
:form-item="inputFormItem"
10696
/>
10797
</UFormField>
10898
</template>

src/app/src/components/form/input/FormInputArray.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ function updateObjectItem(index: number, value: Record<string, unknown>) {
111111
:key="item.label"
112112
variant="subtle"
113113
color="neutral"
114-
size="sm"
115-
class="group/badge flex items-center gap-3 px-2 py-1 min-w-0"
114+
size="xs"
115+
class="min-w-0"
116116
>
117117
<UInput
118118
v-if="activeIndex === item.index"

0 commit comments

Comments
 (0)