Skip to content

Commit 6096e97

Browse files
authored
feat: ui improvements & custom tiles (#78)
* fix: publish example tiles * fix: remove profile menu * feat: improve mobile design * feat: improve ui * feat: improve mobile ui * fix: paddings * feat: secondary menu icon * feat: switch to heroicons * feat: dynamic tile components * docs: form builder
1 parent 486e52b commit 6096e97

20 files changed

Lines changed: 179 additions & 126 deletions

File tree

README.md

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,41 @@ definePageMeta({
180180
});
181181
```
182182

183-
## Icons
183+
## Types
184+
185+
To add types to the database schema of the directus client, add a file `your-extension/index.d.ts` with the following content:
186+
187+
```ts
188+
declare global {
189+
interface CollectivoSchema {
190+
example_collection: ExampleCollection[];
191+
}
192+
193+
interface ExampleCollection {
194+
id: number;
195+
example_field: string;
196+
}
197+
}
184198

185-
Collectivo uses [`nuxt-ui`](https://ui.nuxt.com/getting-started/theming#icons) and [`Iconify`](https://iconify.design/) to load icons. They have to be defined as `i-{collection_name}-{icon_name}`.
199+
export {};
200+
```
186201

187-
By default, Collectivo loads the following to icon libraries:
202+
You can then enjoy type checking when using directus:
188203

189-
- [System UIcons](https://icones.js.org/collection/system-uicons) for the UI
190-
- [Mono Icons](https://icones.js.org/collection/mi) for form components
204+
```ts
205+
const directus = useDirectus();
206+
const data = await directus.request(readItems("example_collection"));
207+
```
191208

192-
Additional libraries can be loaded in `nuxt.config.ts`.
209+
Typescript will then know that data is a `ExampleCollection[]` and that `data[0].example_field` is a string.
210+
211+
## Icons
212+
213+
Collectivo uses [`nuxt-ui`](https://ui.nuxt.com/getting-started/theming#icons) and [`Iconify`](https://iconify.design/) to load icons. They have to be defined as `i-{collection_name}-{icon_name}`. By default, Collectivo uses the [HeroIcons](https://icones.js.org/collection/heroicons) library. Additional libraries can be loaded in [`nuxt.config.ts`](https://ui.nuxt.com/getting-started/theming#icons).
214+
215+
## Dashboard
216+
217+
You can create custom components that can be displayed inside a dashboard tile. To do so, create a new component file `components/global/`. Then, add a new dashboard tile to your database and set the field `Component` to the name of your tile.
193218

194219
# API Reference
195220

@@ -254,13 +279,26 @@ export default defineNuxtPlugin(() => {
254279
const menu = useCollectivoMenus();
255280
menu.value.main.push({
256281
label: "My menu item",
257-
icon: "i-system-uicons-cubes",
282+
icon: "i-heroicons-star",
258283
to: "/my/path",
259284
order: 100
260285
});
261286
}
262287
```
263288
289+
### `CollectivoFormBuilder`
290+
291+
This component can be used to build forms.
292+
293+
Attributes:
294+
295+
- `data: Record<string, any>`: Data to populate the initial form
296+
- `fields: CollectivoFormField[]`: Defines the structure of the form
297+
- `submit: (data: Record<string, any>) => Promise<void>`: Function to be called when form is submitted
298+
- `submit-label: string`: Label of the submit button
299+
300+
To see the different possible form fields, check out the available types of `CollectivoFormField` in `index.d.ts`.
301+
264302
## Backend
265303
266304
The following utility functions can be used for server-side scripts (within `/my-extension/server/`)

collectivo/collectivo/components/collectivo/form/Builder.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
234234
async function onError(event: FormErrorEvent) {
235235
toast.add({
236236
title: t("Some fields are not filled in correctly"),
237-
icon: "i-mi-warning",
237+
icon: "i-heroicons-exclamation-triangle",
238238
color: "red",
239239
timeout: 0,
240240
});
@@ -467,7 +467,7 @@ async function fillOutAll() {
467467
variant="solid"
468468
color="green"
469469
size="lg"
470-
icon="i-mi-circle-check"
470+
icon="i-heroicons-check-16-solid"
471471
:loading="loading"
472472
type="submit"
473473
>

collectivo/collectivo/components/collectivo/form/Page.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async function onSubmit(data: any) {
6262
toast.add({
6363
title: t("There was an error"),
6464
description: err instanceof Error ? err.message : undefined,
65-
icon: "i-mi-warning",
65+
icon: "i-heroicons-exclamation-triangle",
6666
color: "red",
6767
timeout: 0,
6868
});
@@ -83,7 +83,7 @@ async function onSubmit(data: any) {
8383
<slot name="success">
8484
<div class="flex flex-col items-center justify-center space-y-4">
8585
<UIcon
86-
name="i-system-uicons-check"
86+
name="i-heroicons-check-16-solid"
8787
class="w-[64px] h-[64px] text-primary"
8888
/>
8989
<h1 class="text-2xl font-bold text-center">
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<template>Hello World!</template>

collectivo/collectivo/error.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const error = useError();
77
<div
88
class="flex flex-row items-center gap-5 rounded-xl p-[25px] bg-white shadow-lg"
99
>
10-
<UIcon name="i-system-uicons-warning-hex" class="text-6xl text-red-500" />
10+
<UIcon
11+
name="i-heroicons-exclamation-triangle"
12+
class="text-6xl text-red-500"
13+
/>
1114
<div v-if="!error">
1215
<h2>Unknown Error</h2>
1316
<p>Something went wrong.</p>

collectivo/collectivo/index.d.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ declare global {
3131
tags_users: CollectivoUser[] | number[];
3232
}
3333

34-
interface CollectivoTileButton {
35-
id: number;
36-
tiles_label: string;
37-
tiles_path: string;
38-
tiles_is_external: boolean;
39-
}
40-
4134
interface CollectivoTile {
4235
id: number;
4336
sort: number;
@@ -46,6 +39,14 @@ declare global {
4639
tiles_status: "published" | "draft" | "archived";
4740
tiles_buttons: CollectivoTileButton[];
4841
tiles_color: string;
42+
tiles_component: string;
43+
}
44+
45+
interface CollectivoTileButton {
46+
id: number;
47+
tiles_label: string;
48+
tiles_path: string;
49+
tiles_is_external: boolean;
4950
}
5051

5152
interface CollectivoExtension {

collectivo/collectivo/layouts/components/MenuItem.vue

Lines changed: 27 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,29 @@ async function filterItem(item: CollectivoMenuItem) {
2323

2424
<template>
2525
<div v-if="visible">
26-
<!-- IF item.to is function -->
2726
<div v-if="item.click">
28-
<a class="item cursor-pointer" @click="item.click()">
29-
<span class="item__icon">
30-
<slot name="icon">
31-
<UIcon v-if="item.icon" :name="item.icon" class="link-icon" />
32-
</slot>
33-
</span>
27+
<a class="item" @click="item.click()">
28+
<slot name="icon">
29+
<UIcon v-if="item.icon" :name="item.icon" class="item__icon" />
30+
</slot>
3431
<span class="item__title">{{ t(item.label) }}</span>
3532
</a>
3633
</div>
3734
<div v-else-if="item.external">
3835
<a :href="item.to" :target="item.target ?? '_blank'" class="item">
39-
<span class="item__icon">
40-
<slot name="icon">
41-
<UIcon v-if="item.icon" :name="item.icon" class="link-icon" />
42-
</slot>
43-
</span>
36+
<slot name="icon">
37+
<UIcon v-if="item.icon" :name="item.icon" class="item__icon" />
38+
</slot>
39+
4440
<span class="item__title">{{ t(item.label) }}</span>
4541
</a>
4642
</div>
4743
<div v-else>
4844
<NuxtLink :to="item.to" class="item">
49-
<span class="item__icon">
50-
<slot name="icon">
51-
<UIcon v-if="item.icon" :name="item.icon" class="link-icon"
52-
/></slot>
53-
</span>
45+
<slot name="icon">
46+
<UIcon v-if="item.icon" :name="item.icon" class="item__icon" />
47+
</slot>
48+
5449
<span class="item__title">{{ t(item.label) }}</span>
5550
</NuxtLink>
5651
</div>
@@ -59,56 +54,36 @@ async function filterItem(item: CollectivoMenuItem) {
5954

6055
<style lang="scss">
6156
.item {
62-
@apply flex flex-col items-center p-3 mb-1 rounded-xl transition-all;
57+
@apply flex flex-col items-center px-3 py-4 mb-2 rounded-xl transition-all cursor-pointer min-w-20;
6358
&__icon {
64-
@apply block mb-1;
59+
@apply h-5 w-5 lg:h-6 lg:w-6 mb-2;
6560
}
6661
6762
&__title {
6863
@apply md:text-xs lg:text-sm font-semibold;
6964
letter-spacing: 0.28px;
7065
}
7166
72-
&:hover {
73-
@apply bg-primary-50;
74-
.item__title {
75-
@apply text-primary-900;
76-
}
77-
78-
.item__icon {
79-
.link-icon {
80-
@apply text-primary-900;
81-
}
82-
}
83-
}
84-
67+
&:hover,
8568
&.router-link-exact-active {
86-
@apply bg-primary-50;
87-
.item__title {
88-
@apply text-primary-900;
89-
}
90-
91-
.item__icon {
92-
.link-icon {
93-
@apply text-primary-900;
94-
}
95-
}
69+
@apply bg-primary-50 text-primary-900;
9670
}
9771
}
9872
99-
.link-icon {
100-
@apply h-7 w-7 md:h-6 lg:h-[30px] md:w-6 lg:w-[30px];
101-
}
102-
10373
.mobile-menu-item {
10474
@apply p-0 mb-0;
105-
.item__icon {
106-
@apply mb-[7px];
107-
}
10875
109-
.item__title {
110-
@apply text-xs;
111-
letter-spacing: 0.24px;
76+
.item {
77+
@apply py-2.5 px-2.5 mb-0 mx-0;
78+
79+
&__icon {
80+
@apply mb-1;
81+
}
82+
83+
&__title {
84+
@apply text-xs;
85+
letter-spacing: 0.24px;
86+
}
11287
}
11388
11489
&.router-link-exact-active {

collectivo/collectivo/layouts/components/MobileHeader.vue

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,53 @@
22
import Logo from "./Logo.vue";
33
import ProfileMenu from "./ProfileMenu.vue";
44
import PageTitle from "./PageTitle.vue";
5+
6+
const pageTitle = useCollectivoTitle();
7+
const { t } = useI18n();
8+
9+
const scrollY = ref(0);
10+
11+
const updateScroll = () => {
12+
scrollY.value = window.scrollY;
13+
};
14+
15+
onMounted(() => {
16+
window.addEventListener("scroll", updateScroll);
17+
});
18+
19+
onUnmounted(() => {
20+
window.removeEventListener("scroll", updateScroll);
21+
});
22+
23+
const headerClass = computed(() =>
24+
scrollY.value === 0 ? "mobile-header" : "mobile-header border-bottom",
25+
);
526
</script>
627

728
<template>
8-
<div class="mobile-header">
9-
<PageTitle />
10-
<ProfileMenu />
29+
<div :class="headerClass">
30+
<div class="mobile-page-title">
31+
{{ t(pageTitle) }}
32+
</div>
33+
<div class="pt-[1px]"><ProfileMenu /></div>
1134
</div>
35+
<div class="h-header"></div>
1236
</template>
1337

1438
<style lang="scss" scoped>
39+
.mobile-page-title {
40+
@apply text-2xl font-bold;
41+
}
42+
43+
.border-bottom {
44+
@apply border-b-[1px] bg-white border-gray-200;
45+
}
46+
47+
.h-header {
48+
@apply h-[60px] md:hidden;
49+
}
50+
1551
.mobile-header {
16-
@apply px-[25px] py-5 md:hidden flex items-center justify-between;
52+
@apply h-[68px] px-[25px] pt-[20px] md:hidden flex items-start justify-between transition fixed top-0 w-full z-10;
1753
}
1854
</style>

collectivo/collectivo/layouts/components/MobileMenu.vue

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,8 @@ const publicMenuItems = Object.values(menus.value.public).sort(
3333
</template>
3434

3535
<style lang="scss" scoped>
36-
.mobile-menu-item {
37-
@apply w-20;
38-
}
3936
.mobile-menu {
40-
@apply bg-white px-3 pt-2 pb-1 fixed bottom-0 w-full z-10 md:hidden border-t-2 border-gray-500;
37+
@apply bg-white px-3 py-[5px] fixed bottom-0 w-full z-10 md:hidden border-t-[1px] border-gray-200;
4138
box-shadow: 4px 0px 48px 0px rgba(220, 226, 239, 0.5);
4239
4340
&__inner {

collectivo/collectivo/layouts/components/ProfileMenu.vue

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,30 +48,16 @@ for (const [key, value] of Object.entries(locales)) {
4848
</script>
4949

5050
<template>
51-
<template v-if="!user.isAuthenticated">
52-
<UDropdown
53-
:items="topRightMenuNoAuthItems"
54-
:popper="{ placement: 'bottom-start' }"
55-
>
56-
<UIcon class="icon" name="i-system-uicons-translate"></UIcon>
51+
<UDropdown
52+
:items="user.isAuthenticated ? topRightMenuItems : topRightMenuNoAuthItems"
53+
:popper="{ placement: 'bottom-start' }"
54+
>
55+
<UIcon class="icon" name="i-heroicons-bars-3-16-solid" />
5756

58-
<template #item="{ item }">
59-
<span>{{ t(item.label) }}</span>
60-
</template>
61-
</UDropdown>
62-
</template>
63-
<template v-else>
64-
<UDropdown
65-
:items="topRightMenuItems"
66-
:popper="{ placement: 'bottom-start' }"
67-
>
68-
<UIcon class="icon" name="i-system-uicons-user-male-circle" />
69-
70-
<template #item="{ item }">
71-
<span>{{ t(item.label) }}</span>
72-
</template>
73-
</UDropdown>
74-
</template>
57+
<template #item="{ item }">
58+
<span>{{ t(item.label) }}</span>
59+
</template>
60+
</UDropdown>
7561
</template>
7662

7763
<style lang="scss" scoped>

0 commit comments

Comments
 (0)