DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -95,12 +95,7 @@ const sport = ref(false)
|
||||
<div class="mt-5">
|
||||
<Box>
|
||||
<CarouselItemGroup>
|
||||
<CarouselItem
|
||||
as-child
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 4)"
|
||||
:key="fakerKey"
|
||||
:index="fakerKey"
|
||||
>
|
||||
<CarouselItem as-child v-for="(faker, fakerKey) in _.take(fakers, 4)" :key="fakerKey" :index="fakerKey">
|
||||
<div>
|
||||
<div class="truncate text-base font-medium">
|
||||
{{ faker['news'][0]!['title'] }}
|
||||
@@ -113,12 +108,7 @@ const sport = ref(false)
|
||||
<Button variant="ghost" type="button" size="sm" class="border border-foreground/20">
|
||||
View Notes
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="ms-auto border border-foreground/20"
|
||||
type="button"
|
||||
size="sm"
|
||||
>
|
||||
<Button variant="ghost" class="ms-auto border border-foreground/20" type="button" size="sm">
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import fakers from '@/utils/faker'
|
||||
import _ from 'lodash'
|
||||
import { Lucide, type Icon } from '@/components/ui/lucide'
|
||||
import {
|
||||
resolveNotificationRoute,
|
||||
useNotifications,
|
||||
type NotificationItem,
|
||||
} from '@/modules/notification'
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
@@ -11,6 +14,60 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
notifications,
|
||||
unreadCount,
|
||||
loading,
|
||||
error,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
} = useNotifications()
|
||||
|
||||
function getNotificationIcon(notification: NotificationItem): Icon {
|
||||
if (notification.type === 'user_activation_required') {
|
||||
return 'UserPlus'
|
||||
}
|
||||
|
||||
return 'Bell'
|
||||
}
|
||||
|
||||
function getIconClass(color: string): string {
|
||||
switch (color) {
|
||||
case 'success':
|
||||
return 'bg-success/15 text-success'
|
||||
case 'warning':
|
||||
return 'bg-warning/15 text-warning'
|
||||
case 'info':
|
||||
return 'bg-info/15 text-info'
|
||||
case 'purple':
|
||||
return 'bg-purple-500/15 text-purple-600 dark:text-purple-400'
|
||||
case 'orange':
|
||||
return 'bg-orange-500/15 text-orange-600 dark:text-orange-400'
|
||||
default:
|
||||
return 'bg-primary/15 text-primary'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNotificationClick(
|
||||
event: MouseEvent,
|
||||
notification: NotificationItem,
|
||||
) {
|
||||
event.preventDefault()
|
||||
|
||||
await markAsRead(notification.id)
|
||||
|
||||
const route = resolveNotificationRoute(notification)
|
||||
if (route) {
|
||||
await router.push(route)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMarkAllAsRead(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
await markAllAsRead()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,29 +82,65 @@ const props = defineProps<Props>()
|
||||
:class="`before:shadow-foreground/5 z-50 flex w-96 flex-col gap-2.5 px-6 py-5 before:rounded-2xl before:shadow-xl before:backdrop-blur after:rounded-2xl ${props.boxClass ?? ''}`"
|
||||
>
|
||||
<div class="flex place-content-between items-center">
|
||||
<div class="font-medium">Notifications</div>
|
||||
<a class="text-primary" href="">View More</a>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-col gap-2.5">
|
||||
<a
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 5)"
|
||||
:key="fakerKey"
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-2 flex items-center gap-3.5 rounded-2xl border border-transparent p-2"
|
||||
href=""
|
||||
<div class="font-medium">
|
||||
Notifikasi
|
||||
<span v-if="unreadCount > 0" class="text-primary ml-1 text-sm">({{ unreadCount }})</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="unreadCount > 0"
|
||||
type="button"
|
||||
class="text-primary text-sm hover:underline"
|
||||
@click="handleMarkAllAsRead"
|
||||
>
|
||||
<AvatarRoot class="size-11 flex-none rounded-full">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][2]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex place-content-between items-center">
|
||||
<div class="font-medium">{{ faker['users'][0]!['name'] }}</div>
|
||||
<div class="text-xs opacity-70">{{ faker['times'][0] }}</div>
|
||||
Tandakan semua dibaca
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-foreground/70 mt-1 py-6 text-center text-sm">
|
||||
Memuatkan notifikasi...
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="text-danger mt-1 py-4 text-center text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="notifications.length === 0"
|
||||
class="text-foreground/70 mt-1 flex flex-col items-center gap-2 py-8 text-center text-sm"
|
||||
>
|
||||
<Lucide class="size-8 opacity-40" icon="Bell" />
|
||||
<span>Tiada notifikasi buat masa ini.</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-1 flex max-h-96 flex-col gap-1 overflow-y-auto">
|
||||
<a
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="hover:border-foreground/10 hover:bg-foreground/5 -mx-2 flex items-start gap-3.5 rounded-2xl border border-transparent p-2"
|
||||
:class="!notification.is_read && 'bg-primary/5'"
|
||||
href=""
|
||||
@click="handleNotificationClick($event, notification)"
|
||||
>
|
||||
<div
|
||||
class="flex size-11 flex-none items-center justify-center rounded-full"
|
||||
:class="getIconClass(notification.color)"
|
||||
>
|
||||
<Lucide class="size-5" :icon="getNotificationIcon(notification)" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 flex-col gap-1">
|
||||
<div class="flex place-content-between items-start gap-2">
|
||||
<div class="font-medium leading-snug">{{ notification.title }}</div>
|
||||
<div class="shrink-0 text-xs opacity-70">{{ notification.time_ago }}</div>
|
||||
</div>
|
||||
<div class="line-clamp-2 opacity-70">
|
||||
{{ faker['news'][0]!['shortContent'] }}
|
||||
<div class="line-clamp-2 text-sm opacity-70">
|
||||
{{ notification.message }}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="!notification.is_read"
|
||||
class="bg-primary mt-2 size-2 shrink-0 rounded-full"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -77,75 +77,67 @@ const sport = ref(false)
|
||||
<div class="block h-10 items-center sm:flex">
|
||||
<h2 class="me-5 truncate text-lg font-medium">Official Store</h2>
|
||||
<div class="relative mt-3 sm:ms-auto sm:mt-0">
|
||||
<Lucide
|
||||
class="[--color:var(--color-foreground)]/50 absolute inset-y-0 start-0 z-10 my-auto ms-3"
|
||||
icon="MapPin"
|
||||
/>
|
||||
<Lucide class="[--color:var(--color-foreground)]/50 absolute inset-y-0 start-0 z-10 my-auto ms-3" icon="MapPin" />
|
||||
<Input class="box pl-10 sm:w-56" type="text" placeholder="Filter by city" />
|
||||
</div>
|
||||
</div>
|
||||
<Box class="mt-12 p-5 sm:mt-5">
|
||||
<div>250 Official stores in 21 countries, click the marker to see location details.</div>
|
||||
<Map
|
||||
class="mt-5 w-full h-[327px]"
|
||||
:center="[13.388, 52.517]"
|
||||
:zoom="14"
|
||||
:markers="{
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Cafe Berlin', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.388, 52.517] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Restaurant Alex', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.39, 52.518] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Coffee House', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.385, 52.515] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Pizza Place', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.392, 52.519] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Burger Joint', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.387, 52.516] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Starbucks', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.391, 52.52] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Sushi Bar', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.395, 52.522] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Bakery', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.383, 52.514] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Italian Restaurant', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.398, 52.525] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Tea House', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.38, 52.512] },
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
<Map class="mt-5 w-full h-[327px]" :center="[13.388, 52.517]" :zoom="14" :markers="{
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Cafe Berlin', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.388, 52.517] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Restaurant Alex', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.39, 52.518] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Coffee House', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.385, 52.515] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Pizza Place', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.392, 52.519] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Burger Joint', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.387, 52.516] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Starbucks', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.391, 52.52] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Sushi Bar', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.395, 52.522] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Bakery', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.383, 52.514] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Italian Restaurant', type: 'restaurant' },
|
||||
geometry: { type: 'Point', coordinates: [13.398, 52.525] },
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: { name: 'Tea House', type: 'cafe' },
|
||||
geometry: { type: 'Point', coordinates: [13.38, 52.512] },
|
||||
},
|
||||
],
|
||||
}" />
|
||||
</Box>
|
||||
<!-- END: Official Stores -->
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -79,12 +79,9 @@ const sport = ref(false)
|
||||
<a class="text-primary ms-auto truncate" href=""> Show More </a>
|
||||
</div>
|
||||
<div
|
||||
class="before:bg-foreground/10 relative mt-5 before:absolute before:ms-5 before:mt-5 before:block before:h-[85%] before:w-px"
|
||||
>
|
||||
class="before:bg-foreground/10 relative mt-5 before:absolute before:ms-5 before:mt-5 before:block before:h-[85%] before:w-px">
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<div class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20">
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[9]!['photos'][0]" alt="avatar" />
|
||||
@@ -101,9 +98,7 @@ const sport = ref(false)
|
||||
</Box>
|
||||
</div>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<div class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20">
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[8]!['photos'][0]" alt="avatar" />
|
||||
@@ -137,9 +132,7 @@ const sport = ref(false)
|
||||
</div>
|
||||
<div class="my-4 text-center text-xs opacity-70">12 November</div>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<div class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20">
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[7]!['photos'][0]" alt="avatar" />
|
||||
@@ -162,9 +155,7 @@ const sport = ref(false)
|
||||
</Box>
|
||||
</div>
|
||||
<div class="relative mb-3 flex items-center">
|
||||
<div
|
||||
class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20"
|
||||
>
|
||||
<div class="before:bg-foreground/10 before:absolute before:ms-5 before:mt-5 before:block before:h-px before:w-20">
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[6]!['photos'][0]" alt="avatar" />
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -141,22 +141,22 @@ const sport = ref(false)
|
||||
<div class="mt-5 border-t border-foreground/10 pt-5">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-pending)]"
|
||||
></div>
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-pending)]">
|
||||
</div>
|
||||
<span class="truncate">UI/UX Workshop</span>
|
||||
<span class="font-medium xl:ms-auto">23th</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center">
|
||||
<div
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-primary)]"
|
||||
></div>
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-primary)]">
|
||||
</div>
|
||||
<span class="truncate"> VueJs Frontend Development </span>
|
||||
<span class="font-medium xl:ms-auto">10th</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center">
|
||||
<div
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-warning)]"
|
||||
></div>
|
||||
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-warning)]">
|
||||
</div>
|
||||
<span class="truncate">Laravel Rest API</span>
|
||||
<span class="font-medium xl:ms-auto">31th</span>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -78,10 +78,7 @@ const sport = ref(false)
|
||||
<h2 class="me-5 truncate text-lg font-medium">Transactions</h2>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<Box
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 5)"
|
||||
class="mb-3 flex items-center px-5 py-3 before:hidden"
|
||||
>
|
||||
<Box v-for="(faker, fakerKey) in _.take(fakers, 5)" class="mb-3 flex items-center px-5 py-3 before:hidden">
|
||||
<AvatarRoot class="size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][0]" alt="avatar" />
|
||||
@@ -92,12 +89,10 @@ const sport = ref(false)
|
||||
{{ faker['dates'][0] }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'text-success': faker['trueFalse'][0],
|
||||
'text-danger': !faker['trueFalse'][0],
|
||||
}"
|
||||
>
|
||||
<div :class="{
|
||||
'text-success': faker['trueFalse'][0],
|
||||
'text-danger': !faker['trueFalse'][0],
|
||||
}">
|
||||
{{ faker['trueFalse'][0] ? '+' : '-' }}${{ faker['totals'][0] }}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { datePickerMonthSelect } from "@mykopkb/core/styles/datepicker.styles";
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/components/ui/native-select";
|
||||
import type { Api } from "@zag-js/date-picker";
|
||||
import { inject } from "vue";
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/components/ui/native-select";
|
||||
|
||||
import { datePickerYearSelect } from "@mykopkb/core/styles/datepicker.styles";
|
||||
import type { Api } from "@zag-js/date-picker";
|
||||
import { inject } from "vue";
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { input } from "@mykopkb/core/styles/input.styles";
|
||||
import { nativeSelect } from "@mykopkb/core/styles/native-select.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select :class="cn(input, nativeSelect, className)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</select>
|
||||
</template>
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { nativeSelectOption } from "@mykopkb/core/styles/native-select.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<option :class="cn(nativeSelectOption, className)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</option>
|
||||
</template>
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { NativeSelectOptGroup } from "@mykopkb/core/styles/native-select.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<optgroup :class="cn(NativeSelectOptGroup, className)" v-bind="{ ...props, ...$attrs }">
|
||||
<slot />
|
||||
</optgroup>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as NativeSelect } from "./NativeSelect.vue";
|
||||
export { default as NativeSelectOption } from "./NativeSelectOption.vue";
|
||||
export { default as NativeSelectOptionGroup } from "./NativeSelectOptionGroup.vue";
|
||||
@@ -2,11 +2,13 @@
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { textarea } from "@mykopkb/core/styles/textarea.styles";
|
||||
|
||||
const { class: className, ...props } = defineProps<{
|
||||
const modelValue = defineModel<string>();
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea :class="cn(textarea, className)" v-bind="{ ...props }" />
|
||||
<textarea v-model="modelValue" :class="cn(textarea, className)" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
@@ -43,10 +43,17 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import * as select from '@zag-js/select';
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from '@/components/ui/native-select';
|
||||
SelectRoot,
|
||||
SelectControl,
|
||||
SelectTrigger,
|
||||
SelectValueText,
|
||||
SelectContent,
|
||||
SelectItemGroup,
|
||||
SelectItem,
|
||||
SelectItemText,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
// Type definitions
|
||||
type ColumnType = 'index' | 'select' | 'action' | 'status' | 'id' | 'date' | 'email' | 'phone' | 'priority' | 'category' | 'registration' | 'model' | 'unit' | 'text';
|
||||
@@ -129,6 +136,22 @@ const resizingColumn = ref<string | null>(null);
|
||||
const startX = ref(0);
|
||||
const perPageOptions = [10, 25, 50, 100, 250, 500, 1000];
|
||||
|
||||
type PerPageSelectOption = { label: string; value: number };
|
||||
|
||||
const perPageSelectOptions: PerPageSelectOption[] = perPageOptions.map((option) => ({
|
||||
label: String(option),
|
||||
value: option,
|
||||
}));
|
||||
|
||||
const perPageCollection = select.collection({
|
||||
items: perPageSelectOptions,
|
||||
itemToValue: (item) => item.label,
|
||||
});
|
||||
|
||||
const perPageInitial = computed(() => [
|
||||
String(itemsPerPageModel.value ?? props.pagination?.per_page ?? props.itemsPerPage ?? 10),
|
||||
]);
|
||||
|
||||
const startWidth = ref(0);
|
||||
|
||||
// Helper function to detect column type from key and title
|
||||
@@ -136,9 +159,10 @@ const detectColumnType = (header: TableHeader): ColumnType => {
|
||||
const key = header.key.toLowerCase();
|
||||
const title = header.title.toLowerCase();
|
||||
|
||||
// Index/Number columns
|
||||
if (key === '#' || key === 'index' || key === 'bil' || key === 'no' ||
|
||||
title.includes('bil') || title.includes('no') || title.includes('#')) {
|
||||
// Index/Number columns (Bil./# row counters — not field labels like "No. Permohonan")
|
||||
if (key === '#' || key === 'index' || key === 'bil' ||
|
||||
title.includes('bil') || title === '#' ||
|
||||
/^no\.?\s*$/i.test(title.trim())) {
|
||||
return 'index';
|
||||
}
|
||||
|
||||
@@ -416,8 +440,9 @@ const headerAlignClass = (align?: TableHeader['align']) => {
|
||||
return 'text-left';
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (event: Event) => {
|
||||
const value = Number((event.target as HTMLSelectElement).value);
|
||||
const setItemsPerPageValue = (details: { value: string[] }) => {
|
||||
const value = Number(details.value[0]);
|
||||
if (Number.isNaN(value)) return;
|
||||
itemsPerPageModel.value = value;
|
||||
emit('update:items-per-page', value);
|
||||
};
|
||||
@@ -791,12 +816,30 @@ watch(sortBy, (newSort) => {
|
||||
class="flex flex-wrap items-center justify-between gap-4 border-t border-foreground/10 bg-foreground/2 px-4 py-3">
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Bilangan rekod per halaman:</span>
|
||||
<NativeSelect class="h-9 w-auto min-w-20 px-2 py-1 text-sm"
|
||||
:value="String(itemsPerPageModel ?? pagination.per_page)" @change="handleItemsPerPageChange">
|
||||
<NativeSelectOption v-for="option in perPageOptions" :key="option" :value="String(option)">
|
||||
{{ option }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<SelectRoot
|
||||
:key="perPageInitial[0]"
|
||||
class="w-auto min-w-20"
|
||||
:collection="perPageCollection"
|
||||
:default-value="perPageInitial"
|
||||
@value-change="setItemsPerPageValue"
|
||||
>
|
||||
<SelectControl>
|
||||
<SelectTrigger class="h-9 w-auto min-w-20">
|
||||
<SelectValueText />
|
||||
</SelectTrigger>
|
||||
</SelectControl>
|
||||
<SelectContent>
|
||||
<SelectItemGroup>
|
||||
<SelectItem
|
||||
v-for="item in perPageCollection.items"
|
||||
:key="item.label"
|
||||
:item="item"
|
||||
>
|
||||
<SelectItemText>{{ item.label }}</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectItemGroup>
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-muted-foreground">
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
NativeSelectOptionGroup,
|
||||
} from "@/components/ui/native-select";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-20">
|
||||
<div class="grid sm:grid-cols-2">
|
||||
<div
|
||||
class="justify-center items-center flex gap-2 border-b border-e border-foreground/10 p-5 flex-wrap"
|
||||
>
|
||||
<NativeSelect class="w-56">
|
||||
<NativeSelectOption value="">Select status</NativeSelectOption>
|
||||
<NativeSelectOption value="todo">Todo</NativeSelectOption>
|
||||
<NativeSelectOption value="in-progress"
|
||||
>In Progress</NativeSelectOption
|
||||
>
|
||||
<NativeSelectOption value="done">Done</NativeSelectOption>
|
||||
<NativeSelectOption value="cancelled">Cancelled</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<div
|
||||
class="justify-center items-center flex gap-2 border-b border-e border-foreground/10 p-5 flex-wrap"
|
||||
>
|
||||
<NativeSelect class="w-56">
|
||||
<NativeSelectOption value="">Select department</NativeSelectOption>
|
||||
<NativeSelectOptionGroup label="Engineering">
|
||||
<NativeSelectOption value="frontend">Frontend</NativeSelectOption>
|
||||
<NativeSelectOption value="backend">Backend</NativeSelectOption>
|
||||
<NativeSelectOption value="devops">DevOps</NativeSelectOption>
|
||||
</NativeSelectOptionGroup>
|
||||
<NativeSelectOptionGroup label="Sales">
|
||||
<NativeSelectOption value="sales-rep">Sales Rep</NativeSelectOption>
|
||||
<NativeSelectOption value="account-manager">
|
||||
Account Manager
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="sales-director">
|
||||
Sales Director
|
||||
</NativeSelectOption>
|
||||
</NativeSelectOptionGroup>
|
||||
<NativeSelectOptionGroup label="Operations">
|
||||
<NativeSelectOption value="support">
|
||||
Customer Support
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="product-manager">
|
||||
Product Manager
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="ops-manager">
|
||||
Operations Manager
|
||||
</NativeSelectOption>
|
||||
</NativeSelectOptionGroup>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<div
|
||||
class="justify-center items-center flex gap-2 border-b border-e border-foreground/10 p-5 flex-wrap"
|
||||
>
|
||||
<NativeSelect class="w-56" disabled>
|
||||
<NativeSelectOption value="">Select status</NativeSelectOption>
|
||||
<NativeSelectOption value="todo">Todo</NativeSelectOption>
|
||||
<NativeSelectOption value="in-progress"
|
||||
>In Progress</NativeSelectOption
|
||||
>
|
||||
<NativeSelectOption value="done">Done</NativeSelectOption>
|
||||
<NativeSelectOption value="cancelled">Cancelled</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -78,10 +78,7 @@ const sport = ref(false)
|
||||
<h2 class="me-5 truncate text-lg font-medium">Weekly Best Sellers</h2>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<Box
|
||||
v-for="(faker, fakerKey) in _.take(fakers, 4)"
|
||||
class="mb-3 flex items-center px-4 py-4 before:hidden"
|
||||
>
|
||||
<Box v-for="(faker, fakerKey) in _.take(fakers, 4)" class="mb-3 flex items-center px-4 py-4 before:hidden">
|
||||
<AvatarRoot class="rounded-xl size-11">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][0]" alt="avatar" />
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
CarouselRoot,
|
||||
@@ -147,12 +146,7 @@ const sport = ref(false)
|
||||
</Table>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center sm:flex-row sm:flex-nowrap">
|
||||
<PaginationRoot
|
||||
:count="5000"
|
||||
:pageSize="10"
|
||||
:siblingCount="2"
|
||||
class="w-full sm:me-auto sm:w-auto"
|
||||
>
|
||||
<PaginationRoot :count="5000" :pageSize="10" :siblingCount="2" class="w-full sm:me-auto sm:w-auto">
|
||||
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
|
||||
<PaginationContext v-slot="{ pagination }">
|
||||
<template v-for="(page, index) in pagination?.pages" :key="index">
|
||||
|
||||
Reference in New Issue
Block a user