@finema/finework-layer 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,220 +1,220 @@
1
- <template>
2
- <div class="relative flex min-h-screen flex-1">
3
- <div
4
- :class="[`
5
- fixed inset-0 z-30 hidden w-auto
6
- lg:block
7
- `, {
8
- 'max-w-[88px]': isCollapsed,
9
- 'max-w-[260px]': !isCollapsed,
10
- }]"
11
- >
12
- <Sidebar
13
- :label="label"
14
- :is-collapsed="isCollapsed"
15
- :items="sidebarItems"
16
- :is-group="isSidebarGroup"
17
- @toggle-collapsed="isCollapsed = !isCollapsed"
18
- />
19
- </div>
20
- <div
21
- :class="['w-full bg-gray-50', {
22
- 'lg:pl-[88px]': isCollapsed,
23
- 'lg:pl-[260px]': !isCollapsed,
24
- }]"
25
- >
26
- <nav
27
- class="
28
- fixed top-0 left-0 z-20 flex min-h-[64px] w-screen items-center justify-between
29
- gap-4 bg-white px-5 lg:min-h-[72px]
30
- lg:justify-end
31
- "
32
- style="box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.04);"
33
- >
34
- <div class="flex items-center gap-4">
35
- <Slideover
36
- v-model:open="isShowSidebarMobile"
37
- :ui="{
38
- content: 'w-[80%] max-w-[260px] lg:hidden',
39
- overlay: 'lg:hidden',
40
- }"
41
- side="left"
42
- >
43
- <svg
44
- class="
45
- cursor-pointer
46
- lg:hidden
47
- "
48
- width="19"
49
- height="18"
50
- viewBox="0 0 19 18"
51
- fill="none"
52
- xmlns="http://www.w3.org/2000/svg"
53
- transform="scale(-1, 1)"
54
- @click="isShowSidebarMobile = true"
55
- >
56
- <path
57
- d="M10 18L10 16L19 16L19 18L10 18ZM6 2L6 -2.62268e-07L19 -8.30516e-07L19 2L6 2ZM-3.49691e-07 10L-4.37114e-07 8L19 8L19 10L-3.49691e-07 10Z"
58
- fill="#4B5563"
59
- />
60
- </svg>
61
-
62
- <template #content>
63
- <Sidebar
64
- :label="label"
65
- :is-collapsed="false"
66
- :is-mobile="true"
67
- :items="sidebarItems"
68
- :is-group="isSidebarGroup"
69
- @toggle-collapsed="isShowSidebarMobile = false"
70
- />
71
- </template>
72
- </Slideover>
73
- <NuxtLink :to="routes.home.to">
74
- <img
75
- src="/logo-mini.png"
76
- alt="logo_mini_color"
77
- class="
78
- w-[126px] min-w-[126px] lg:hidden
79
- lg:w-[172px] lg:min-w-[172px]
80
- "
81
- />
82
- </NuxtLink>
83
- <Apps class="lg:hidden" />
84
- </div>
85
-
86
- <div class="flex items-center justify-center gap-4">
87
- <DropdownMenu
88
- size="xl"
89
- :items="userMenuItems"
90
- :content="{
91
- align: 'end',
92
- side: 'bottom',
93
- }"
94
- :ui="{
95
- content: 'w-48',
96
- }"
97
- >
98
- <div class="relative flex cursor-pointer items-center gap-2">
99
- <div
100
- class="hidden flex-col justify-end text-right lg:flex"
101
- >
102
- <p class="font-bold">
103
- {{ auth.me.value?.display_name || auth.me.value?.full_name }}
104
- </p>
105
- <p class="text-muted text-sm">
106
- {{ auth.me.value?.email || '' }}
107
- </p>
108
- </div>
109
- <Avatar
110
- class="border-muted size-[40px] border text-2xl"
111
- icon="ri:user-line"
112
- :src="auth.me.value?.avatar_url || ''"
113
- />
114
- <Icon
115
- name="i-ph:caret-down-light"
116
- class="size-5"
117
- />
118
- </div>
119
- </DropdownMenu>
120
- </div>
121
- </nav>
122
- <div class="w-full bg-gray-50 pt-[64px] lg:pt-[72px]">
123
- <main
124
- class="
125
- mx-auto min-h-full w-full flex-1 px-6 py-10 lg:px-8
126
- "
127
- >
128
- <Breadcrumb
129
- v-if="!app.pageMeta.isHideBreadcrumbs && breadcrumbsItems.length > 1"
130
- :items="breadcrumbsItems"
131
- class="mb-6"
132
- :ui="{
133
- item: 'max-w-2/3',
134
- list: 'w-full',
135
- }"
136
- />
137
- <div
138
- v-if="app.pageMeta.title"
139
- class="
140
- mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4
141
- lg:flex-row lg:items-start
142
- "
143
- >
144
- <div class="flex flex-1 flex-col">
145
- <h1
146
- class="
147
- text-3xl font-bold wrap-break-word
148
- lg:max-w-2/3
149
- "
150
- :title="app.pageMeta.title"
151
- >
152
- {{ app.pageMeta.title }}
153
- <span id="page-title-extra" />
154
- </h1>
155
-
156
- <div id="page-subtitle" />
157
- <p
158
- v-if="app.pageMeta.sub_title"
159
- class="text-[#475467]"
160
- >
161
- {{ app.pageMeta.sub_title }}
162
- </p>
163
- </div>
164
- <div id="page-header" />
165
- </div>
166
- <slot />
167
- </main>
168
- </div>
169
- </div>
170
- </div>
171
- </template>
172
-
173
- <script lang="ts" setup>
174
- import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
175
- import { computed } from 'vue'
176
- import Sidebar from './Sidebar.vue'
177
- import Apps from './Apps.vue'
178
-
179
- defineProps<{
180
- label: string
181
- sidebarItems: NavigationMenuItem[]
182
- isSidebarGroup?: boolean
183
- }>()
184
-
185
- const app = useApp()
186
- const isShowSidebarMobile = ref(false)
187
- const auth = useAuth()
188
- // Cookie to store user preference for desktop
189
- const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
190
- default: () => false,
191
- path: '/',
192
- })
193
-
194
- const userMenuItems: DropdownMenuItem[] = [
195
- {
196
- label: 'View profile',
197
- icon: 'i-lucide-user',
198
- to: routes.account.profile.to,
199
- },
200
-
201
- {
202
- label: routes.logout.label,
203
- icon: 'i-lucide-log-out',
204
- to: routes.logout.to,
205
- external: true,
206
- },
207
- ]
208
-
209
- const breadcrumbsItems = computed(() => [
210
- // {
211
- // label: '',
212
- // icon: 'ph:house',
213
- // to: '/',
214
- // },
215
- ...(app.pageMeta.breadcrumbs || []).map((item: any) => ({
216
- ...item,
217
- icon: '',
218
- })),
219
- ])
220
- </script>
1
+ <template>
2
+ <div class="relative flex min-h-screen flex-1">
3
+ <div
4
+ :class="[`
5
+ fixed inset-0 z-30 hidden w-auto
6
+ lg:block
7
+ `, {
8
+ 'max-w-[88px]': isCollapsed,
9
+ 'max-w-[260px]': !isCollapsed,
10
+ }]"
11
+ >
12
+ <Sidebar
13
+ :label="label"
14
+ :is-collapsed="isCollapsed"
15
+ :items="sidebarItems"
16
+ :is-group="isSidebarGroup"
17
+ @toggle-collapsed="isCollapsed = !isCollapsed"
18
+ />
19
+ </div>
20
+ <div
21
+ :class="['w-full bg-gray-50', {
22
+ 'lg:pl-[88px]': isCollapsed,
23
+ 'lg:pl-[260px]': !isCollapsed,
24
+ }]"
25
+ >
26
+ <nav
27
+ class="
28
+ fixed top-0 left-0 z-20 flex min-h-[64px] w-screen items-center justify-between
29
+ gap-4 bg-white px-5 lg:min-h-[72px]
30
+ lg:justify-end
31
+ "
32
+ style="box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.04);"
33
+ >
34
+ <div class="flex items-center gap-4">
35
+ <Slideover
36
+ v-model:open="isShowSidebarMobile"
37
+ :ui="{
38
+ content: 'w-[80%] max-w-[260px] lg:hidden',
39
+ overlay: 'lg:hidden',
40
+ }"
41
+ side="left"
42
+ >
43
+ <svg
44
+ class="
45
+ cursor-pointer
46
+ lg:hidden
47
+ "
48
+ width="19"
49
+ height="18"
50
+ viewBox="0 0 19 18"
51
+ fill="none"
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ transform="scale(-1, 1)"
54
+ @click="isShowSidebarMobile = true"
55
+ >
56
+ <path
57
+ d="M10 18L10 16L19 16L19 18L10 18ZM6 2L6 -2.62268e-07L19 -8.30516e-07L19 2L6 2ZM-3.49691e-07 10L-4.37114e-07 8L19 8L19 10L-3.49691e-07 10Z"
58
+ fill="#4B5563"
59
+ />
60
+ </svg>
61
+
62
+ <template #content>
63
+ <Sidebar
64
+ :label="label"
65
+ :is-collapsed="false"
66
+ :is-mobile="true"
67
+ :items="sidebarItems"
68
+ :is-group="isSidebarGroup"
69
+ @toggle-collapsed="isShowSidebarMobile = false"
70
+ />
71
+ </template>
72
+ </Slideover>
73
+ <NuxtLink :to="routes.home.to">
74
+ <img
75
+ src="/logo-mini.png"
76
+ alt="logo_mini_color"
77
+ class="
78
+ w-[126px] min-w-[126px] lg:hidden
79
+ lg:w-[172px] lg:min-w-[172px]
80
+ "
81
+ />
82
+ </NuxtLink>
83
+ <Apps class="lg:hidden" />
84
+ </div>
85
+
86
+ <div class="flex items-center justify-center gap-4">
87
+ <DropdownMenu
88
+ size="xl"
89
+ :items="userMenuItems"
90
+ :content="{
91
+ align: 'end',
92
+ side: 'bottom',
93
+ }"
94
+ :ui="{
95
+ content: 'w-48',
96
+ }"
97
+ >
98
+ <div class="relative flex cursor-pointer items-center gap-2">
99
+ <div
100
+ class="hidden flex-col justify-end text-right lg:flex"
101
+ >
102
+ <p class="font-bold">
103
+ {{ auth.me.value?.display_name || auth.me.value?.full_name }}
104
+ </p>
105
+ <p class="text-muted text-sm">
106
+ {{ auth.me.value?.email || '' }}
107
+ </p>
108
+ </div>
109
+ <Avatar
110
+ class="border-muted size-[40px] border text-2xl"
111
+ icon="ri:user-line"
112
+ :src="auth.me.value?.avatar_url || ''"
113
+ />
114
+ <Icon
115
+ name="i-ph:caret-down-light"
116
+ class="size-5"
117
+ />
118
+ </div>
119
+ </DropdownMenu>
120
+ </div>
121
+ </nav>
122
+ <div class="w-full bg-gray-50 pt-[64px] lg:pt-[72px]">
123
+ <main
124
+ class="
125
+ mx-auto min-h-full w-full flex-1 px-6 py-10 lg:px-8
126
+ "
127
+ >
128
+ <Breadcrumb
129
+ v-if="!app.pageMeta.isHideBreadcrumbs && breadcrumbsItems.length > 1"
130
+ :items="breadcrumbsItems"
131
+ class="mb-6"
132
+ :ui="{
133
+ item: 'max-w-2/3',
134
+ list: 'w-full',
135
+ }"
136
+ />
137
+ <div
138
+ v-if="app.pageMeta.title"
139
+ class="
140
+ mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4
141
+ lg:flex-row lg:items-start
142
+ "
143
+ >
144
+ <div class="flex flex-1 flex-col">
145
+ <h1
146
+ class="
147
+ text-3xl font-bold wrap-break-word
148
+ lg:max-w-2/3
149
+ "
150
+ :title="app.pageMeta.title"
151
+ >
152
+ {{ app.pageMeta.title }}
153
+ <span id="page-title-extra" />
154
+ </h1>
155
+
156
+ <div id="page-subtitle" />
157
+ <p
158
+ v-if="app.pageMeta.sub_title"
159
+ class="text-[#475467]"
160
+ >
161
+ {{ app.pageMeta.sub_title }}
162
+ </p>
163
+ </div>
164
+ <div id="page-header" />
165
+ </div>
166
+ <slot />
167
+ </main>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </template>
172
+
173
+ <script lang="ts" setup>
174
+ import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
175
+ import { computed } from 'vue'
176
+ import Sidebar from './Sidebar.vue'
177
+ import Apps from './Apps.vue'
178
+
179
+ defineProps<{
180
+ label: string
181
+ sidebarItems: NavigationMenuItem[]
182
+ isSidebarGroup?: boolean
183
+ }>()
184
+
185
+ const app = useApp()
186
+ const isShowSidebarMobile = ref(false)
187
+ const auth = useAuth()
188
+ // Cookie to store user preference for desktop
189
+ const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
190
+ default: () => false,
191
+ path: '/',
192
+ })
193
+
194
+ const userMenuItems: DropdownMenuItem[] = [
195
+ {
196
+ label: 'View profile',
197
+ icon: 'i-lucide-user',
198
+ to: routes.account.profile.to,
199
+ },
200
+
201
+ {
202
+ label: routes.logout.label,
203
+ icon: 'i-lucide-log-out',
204
+ to: routes.logout.to,
205
+ external: true,
206
+ },
207
+ ]
208
+
209
+ const breadcrumbsItems = computed(() => [
210
+ // {
211
+ // label: '',
212
+ // icon: 'ph:house',
213
+ // to: '/',
214
+ // },
215
+ ...(app.pageMeta.breadcrumbs || []).map((item: any) => ({
216
+ ...item,
217
+ icon: '',
218
+ })),
219
+ ])
220
+ </script>
@@ -1,56 +1,56 @@
1
- <template>
2
- <slot v-if="skip" />
3
- <template v-else>
4
- <Card v-if="status.isLoading">
5
- <div class="flex items-center justify-center py-12">
6
- <Loader />
7
- </div>
8
- </Card>
9
-
10
- <!-- Not Found State -->
11
- <Card v-else-if="status.isSuccess && !data || status.isError && status.errorData?.code === 'NOT_FOUND'">
12
- <div class="py-12 text-center">
13
- <Icon
14
- name="lucide:search-x"
15
- class="mx-auto mb-4 text-gray-400"
16
- size="48"
17
- />
18
- <h3 class="mb-2 text-lg font-medium text-gray-900">
19
- ไม่พบข้อมูล
20
- </h3>
21
- <p class="text-gray-600">
22
- ไม่พบข้อมูลที่คุณค้นหา กรุณาลองใหม่อีกครั้ง
23
- </p>
24
- </div>
25
- </Card>
26
- <Card v-else-if="status.isError">
27
- <div class="py-12 text-center">
28
- <Icon
29
- name="lucide:alert-circle"
30
- class="mx-auto mb-4 text-red-500"
31
- size="48"
32
- />
33
- <h3 class="mb-2 text-lg font-medium text-gray-900">
34
- เกิดข้อผิดพลาด
35
- </h3>
36
- <p class="text-gray-600">
37
- ไม่สามารถโหลดข้อมูลได้ กรุณาลองใหม่อีกครั้ง
38
- </p>
39
- </div>
40
- </Card>
41
- <slot
42
- v-else-if="data"
43
- :data="data"
44
- />
45
- </template>
46
- </template>
47
-
48
- <script lang="ts" setup generic="T">
49
- import type { IStatus } from '#core/types/lib'
50
-
51
- defineProps<{
52
- status: IStatus
53
- data: T
54
- skip?: boolean
55
- }>()
56
- </script>
1
+ <template>
2
+ <slot v-if="skip" />
3
+ <template v-else>
4
+ <Card v-if="status.isLoading">
5
+ <div class="flex items-center justify-center py-12">
6
+ <Loader />
7
+ </div>
8
+ </Card>
9
+
10
+ <!-- Not Found State -->
11
+ <Card v-else-if="status.isSuccess && !data || status.isError && status.errorData?.code === 'NOT_FOUND'">
12
+ <div class="py-12 text-center">
13
+ <Icon
14
+ name="lucide:search-x"
15
+ class="mx-auto mb-4 text-gray-400"
16
+ size="48"
17
+ />
18
+ <h3 class="mb-2 text-lg font-medium text-gray-900">
19
+ ไม่พบข้อมูล
20
+ </h3>
21
+ <p class="text-gray-600">
22
+ ไม่พบข้อมูลที่คุณค้นหา กรุณาลองใหม่อีกครั้ง
23
+ </p>
24
+ </div>
25
+ </Card>
26
+ <Card v-else-if="status.isError">
27
+ <div class="py-12 text-center">
28
+ <Icon
29
+ name="lucide:alert-circle"
30
+ class="mx-auto mb-4 text-red-500"
31
+ size="48"
32
+ />
33
+ <h3 class="mb-2 text-lg font-medium text-gray-900">
34
+ เกิดข้อผิดพลาด
35
+ </h3>
36
+ <p class="text-gray-600">
37
+ ไม่สามารถโหลดข้อมูลได้ กรุณาลองใหม่อีกครั้ง
38
+ </p>
39
+ </div>
40
+ </Card>
41
+ <slot
42
+ v-else-if="data"
43
+ :data="data"
44
+ />
45
+ </template>
46
+ </template>
47
+
48
+ <script lang="ts" setup generic="T">
49
+ import type { IStatus } from '#core/types/lib'
50
+
51
+ defineProps<{
52
+ status: IStatus
53
+ data: T
54
+ skip?: boolean
55
+ }>()
56
+ </script>
@@ -8,7 +8,7 @@ export enum Permission {
8
8
  export const PERMISSION_LABEL = {
9
9
  [Permission.ADMIN]: 'Admin',
10
10
  [Permission.USER]: 'User',
11
- [Permission.SUPER]: 'Super Admin',
11
+ [Permission.SUPER]: 'Settings',
12
12
  [Permission.NONE]: 'No Access',
13
13
  }
14
14
 
@@ -166,7 +166,7 @@ export const useAuth = () => {
166
166
 
167
167
  ...(hasPermission(UserModule.SETTING, Permission.SUPER)
168
168
  ? [{
169
- label: 'Super Admin',
169
+ label: 'Settings',
170
170
  icon: '/admin/super-admin-logo.png',
171
171
  to: routes.admin.users.to,
172
172
  }]
@@ -1,50 +1,50 @@
1
- import type { AxiosRequestConfig } from 'axios'
2
-
3
- export const useRequestOptions = () => {
4
- const config = useRuntimeConfig()
5
-
6
- const mock = (): Omit<AxiosRequestConfig, 'baseURL'> & {
7
- baseURL: string
8
- } => {
9
- return {
10
- baseURL: config.public.baseAPIMock || 'http://localhost:3000/api/mock',
11
- }
12
- }
13
-
14
- const base = (): Omit<AxiosRequestConfig, 'baseURL'> & {
15
- baseURL: string
16
- } => {
17
- return {
18
- baseURL: config.public.baseAPI,
19
- }
20
- }
21
-
22
- const auth = (): Omit<AxiosRequestConfig, 'baseURL'> & {
23
- baseURL: string
24
- } => {
25
- return {
26
- baseURL: config.public.baseAPI,
27
- headers: {
28
- Authorization: `Bearer ${useAuth().token.value}`,
29
- },
30
- }
31
- }
32
-
33
- const file = (): Omit<AxiosRequestConfig, 'baseURL'> & {
34
- baseURL: string
35
- } => {
36
- return {
37
- baseURL: config.public.baseAPI + '/uploads',
38
- headers: {
39
- Authorization: `Bearer ${useAuth().token.value}`,
40
- },
41
- }
42
- }
43
-
44
- return {
45
- base,
46
- mock,
47
- auth,
48
- file,
49
- }
50
- }
1
+ import type { AxiosRequestConfig } from 'axios'
2
+
3
+ export const useRequestOptions = () => {
4
+ const config = useRuntimeConfig()
5
+
6
+ const mock = (): Omit<AxiosRequestConfig, 'baseURL'> & {
7
+ baseURL: string
8
+ } => {
9
+ return {
10
+ baseURL: config.public.baseAPIMock || 'http://localhost:3000/api/mock',
11
+ }
12
+ }
13
+
14
+ const base = (): Omit<AxiosRequestConfig, 'baseURL'> & {
15
+ baseURL: string
16
+ } => {
17
+ return {
18
+ baseURL: config.public.baseAPI,
19
+ }
20
+ }
21
+
22
+ const auth = (): Omit<AxiosRequestConfig, 'baseURL'> & {
23
+ baseURL: string
24
+ } => {
25
+ return {
26
+ baseURL: config.public.baseAPI,
27
+ headers: {
28
+ Authorization: `Bearer ${useAuth().token.value}`,
29
+ },
30
+ }
31
+ }
32
+
33
+ const file = (): Omit<AxiosRequestConfig, 'baseURL'> & {
34
+ baseURL: string
35
+ } => {
36
+ return {
37
+ baseURL: config.public.baseAPI + '/uploads',
38
+ headers: {
39
+ Authorization: `Bearer ${useAuth().token.value}`,
40
+ },
41
+ }
42
+ }
43
+
44
+ return {
45
+ base,
46
+ mock,
47
+ auth,
48
+ file,
49
+ }
50
+ }