This commit is contained in:
2024-08-30 18:15:07 +02:00
parent 8cadaf08a0
commit c77503ed45
17 changed files with 273 additions and 149 deletions

View File

@@ -6,18 +6,21 @@ defineProps<{
dropdownItems: { label: string, icon: string, color: string, click: (category: CategoryType) => void }[] dropdownItems: { label: string, icon: string, color: string, click: (category: CategoryType) => void }[]
}>() }>()
defineEmits(['createTab']) defineEmits(['createTab'])
const { canCreateTabInCategory } = await useUserLimit()
</script> </script>
<template> <template>
<div v-if="category" class="flex items-center mb-4" :class="category.nameVisible ? 'justify-between' : 'justify-end'"> <div v-if="category" class="flex items-center mb-4" :class="category.nameVisible ? 'justify-between' : 'justify-end'">
<div v-if="category.nameVisible" class="flex items-center gap-2 mb-4" :class="`text-${category.color}-500`"> <div v-if="category.nameVisible" class="flex items-center gap-2 mb-4" :class="`text-${category.color}-500`">
<UIcon :name="`i-ph:${category.icon}`" size="28" /> <UIcon :name="category.icon" size="28" />
<h1 class="font-bold text-2xl"> <h1 class="font-bold text-2xl">
{{ category.name }} {{ category.name }}
</h1> </h1>
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<UButton <UButton
v-if="canCreateTabInCategory(category.id)"
color="gray" color="gray"
variant="solid" variant="solid"
label="New tab" label="New tab"

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
const colorMode = useColorMode() const colorMode = useColorMode()
const { user, loggedIn, clear } = useUserSession() const { user, loggedIn, clear } = useUserSession()
const isSettingsOpen = ref(false) const isSettingsOpen = ref(false)
const isDark = computed(() => colorMode.preference === 'dark')
const items = [ const items = [
[{ [{
slot: 'account', slot: 'account',
@@ -13,26 +13,28 @@ const items = [
label: 'Home', label: 'Home',
icon: 'i-ph:house-line-duotone', icon: 'i-ph:house-line-duotone',
action: () => navigateTo('/'), action: () => navigateTo('/'),
shortcut: 'H',
}, { }, {
label: 'Settings', label: 'Settings',
icon: 'i-ph:gear-six-duotone', icon: 'i-ph:gear-six-duotone',
action: () => { action: () => isSettingsOpen.value = true,
isSettingsOpen.value = true shortcut: 'S',
},
}, { }, {
label: 'Profile', label: isDark.value ? 'Light mode' : 'Dark mode',
icon: 'i-ph:person-arms-spread-duotone', icon: isDark.value ? 'i-ph:moon-duotone' : 'i-ph:sun-duotone',
action: () => navigateTo('/profile'), action: () => toggleColorMode(),
shortcut: 'T',
}], }],
[{ [{
slot: 'logout', slot: 'logout',
label: 'Sign out', label: 'Sign out',
icon: 'i-ph:sign-out-bold', icon: 'i-ph:sign-out-bold',
shortcut: 'L',
}], }],
] ]
function toggleColorMode() { function toggleColorMode() {
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark' colorMode.preference = isDark.value ? 'light' : 'dark'
} }
async function logout() { async function logout() {
@@ -43,6 +45,8 @@ async function logout() {
defineShortcuts({ defineShortcuts({
t: () => toggleColorMode(), t: () => toggleColorMode(),
s: () => isSettingsOpen.value = !isSettingsOpen.value,
l: async () => await logout(),
}) })
</script> </script>
@@ -58,7 +62,13 @@ defineShortcuts({
</h1> </h1>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<ClientOnly> <ClientOnly>
<UDropdown v-if="loggedIn" :items="items" mode="hover" :ui="{ item: { disabled: 'cursor-text select-text' } }" :popper="{ placement: 'bottom-start' }"> <UDropdown
v-if="loggedIn"
:items="items"
mode="hover"
:ui="{ item: { disabled: 'cursor-text select-text' } }"
:popper="{ placement: 'bottom-end' }"
>
<UAvatar :src="user.avatar" /> <UAvatar :src="user.avatar" />
<template #account> <template #account>
@@ -74,26 +84,28 @@ defineShortcuts({
<template #item="{ item }"> <template #item="{ item }">
<div class="w-full flex justify-between items-center" @click.prevent="item.action()"> <div class="w-full flex justify-between items-center" @click.prevent="item.action()">
<span class="truncate">{{ item.label }}</span> <div class="gap-2 flex items-center">
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" /> <UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
<span class="truncate">{{ item.label }}</span>
</div>
<UKbd v-if="item.shortcut">
{{ item.shortcut }}
</UKbd>
</div> </div>
</template> </template>
<template #logout="{ item }"> <template #logout="{ item }">
<div class="w-full flex justify-between items-center" @click="logout()"> <div class="w-full flex justify-between items-center" @click="logout()">
<span class="truncate">{{ item.label }}</span> <div class="flex gap-2 items-center">
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" /> <UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500" />
<span class="truncate">{{ item.label }}</span>
</div>
<UKbd v-if="item.shortcut">
{{ item.shortcut }}
</UKbd>
</div> </div>
</template> </template>
</UDropdown> </UDropdown>
<UButton
:icon="$colorMode.preference === 'dark' ? 'i-ph:moon-duotone' : 'i-ph:sun-duotone'"
color="gray"
square
size="md"
variant="ghost"
@click="toggleColorMode"
/>
</clientonly> </clientonly>
</div> </div>
</div> </div>
@@ -111,10 +123,12 @@ defineShortcuts({
</template> </template>
<template #default> <template #default>
Hey <div class="space-y-12">
Delete account <div>
Change user details Delete account
{{ user }} Change user details
</div>
</div>
</template> </template>
</UCard> </UCard>
</USlideover> </USlideover>

View File

@@ -5,8 +5,16 @@ defineProps<{
tab: TabType tab: TabType
}>() }>()
const { setTabPrimary } = await useTabs()
// DropDown Items // DropDown Items
const items = [[ const items = [[
{
label: 'Toggle favorite',
icon: 'i-ph:star-duotone',
color: 'yellow',
click: tab => setTabPrimary(tab, !tab.primary),
},
{ {
label: 'Edit', label: 'Edit',
icon: 'i-ph:pencil-duotone', icon: 'i-ph:pencil-duotone',
@@ -56,12 +64,12 @@ function openDeleteTabModal(tab: TabType) {
<div class="flex items-center justify-between h-full"> <div class="flex items-center justify-between h-full">
<div class="flex gap-4 items-center h-full"> <div class="flex gap-4 items-center h-full">
<UBadge :color="tab.color" class="p-2" variant="soft"> <UBadge :color="tab.color" class="p-2" variant="soft">
<UIcon :name="`i-ph:${tab.icon}`" size="32" /> <UIcon :name="tab.icon" size="32" />
</UBadge> </UBadge>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div :class="`text-${tab.color}-400`" class="text-xl font-medium"> <p :class="`text-${tab.color}-400`" class="text-xl font-medium truncate">
<p>{{ tab.name }}</p> {{ tab.name }}
</div> </p>
</div> </div>
</div> </div>
<UDropdown <UDropdown
@@ -71,8 +79,10 @@ function openDeleteTabModal(tab: TabType) {
> >
<UButton <UButton
color="gray" color="gray"
variant="soft" variant="ghost"
icon="i-ph:dots-three-outline-vertical-duotone" :padded="false"
size="sm"
icon="i-ph:dots-three-outline-duotone"
/> />
<template #item="{ item }"> <template #item="{ item }">

View File

@@ -20,6 +20,8 @@ async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
state.icon = undefined state.icon = undefined
state.name = undefined state.name = undefined
} }
const { loading, search } = useIcons()
</script> </script>
<template> <template>
@@ -42,11 +44,35 @@ async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
<template #default> <template #default>
<UForm :schema="CreateCategorySchema" :state="state" class="space-y-4" @submit="handleCreate"> <UForm :schema="CreateCategorySchema" :state="state" class="space-y-4" @submit="handleCreate">
<UFormGroup label="Name" name="name"> <UFormGroup label="Name" name="name">
<UInput v-model="state.name" type="text" variant="outline" /> <UInput v-model="state.name" placeholder="Enter name" type="text" variant="outline" />
</UFormGroup> </UFormGroup>
<UFormGroup label="Icon " name="icon" help="Get icon from the Phosphor Collection"> <UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" variant="outline" /> <USelectMenu
v-model="state.icon"
:loading="loading"
:searchable="search"
placeholder="Select an icon"
searchable-placeholder="Search an icon"
>
<template #label>
<div v-if="state.icon" class="flex items-center gap-2">
<UIcon size="20" :name="`i-${state.icon}`" />
<span class="truncate">{{ state.icon }}</span>
</div>
</template>
<template #option="{ option }">
<div class="flex items-center gap-2">
<UIcon size="20" :name="`i-${option}`" />
<span class="truncate">{{ option }}</span>
</div>
</template>
<template #empty>
Enter an icon name, keyword or tag
</template>
</USelectMenu>
</UFormGroup> </UFormGroup>
<UFormGroup label="Color " name="color"> <UFormGroup label="Color " name="color">

View File

@@ -33,6 +33,8 @@ async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
state.primary = false state.primary = false
state.categoryId = props.category?.id state.categoryId = props.category?.id
} }
const { loading, search } = useIcons()
</script> </script>
<template> <template>
@@ -59,7 +61,31 @@ async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
</UFormGroup> </UFormGroup>
<UFormGroup label="Icon " name="icon"> <UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" /> <USelectMenu
v-model="state.icon"
:loading="loading"
:searchable="search"
placeholder="Select an icon"
searchable-placeholder="Search an icon"
>
<template #label>
<div v-if="state.icon" class="flex items-center gap-2">
<UIcon size="20" :name="`i-${state.icon}`" />
<span class="truncate">{{ state.icon }}</span>
</div>
</template>
<template #option="{ option }">
<div class="flex items-center gap-2">
<UIcon size="20" :name="`i-${option}`" />
<span class="truncate">{{ option }}</span>
</div>
</template>
<template #empty>
Enter an icon name, keyword or tag
</template>
</USelectMenu>
</UFormGroup> </UFormGroup>
<UFormGroup label="Color " name="color"> <UFormGroup label="Color " name="color">

View File

@@ -8,12 +8,13 @@ const props = defineProps<{
const emit = defineEmits(['closeModal']) const emit = defineEmits(['closeModal'])
const { updateCategory } = await useCategories() const { updateCategory } = await useCategories()
const { loading, search } = useIcons()
const state = reactive({ const state = reactive({
name: props.category?.name, name: undefined,
icon: props.category?.icon, icon: undefined,
color: props.category?.color, color: COLORS[0],
nameVisible: props.category?.nameVisible, nameVisible: undefined,
}) })
watchEffect(() => { watchEffect(() => {
@@ -60,9 +61,32 @@ async function handleUpdate(event: FormSubmitEvent<UpdateCategorySchema>) {
</UFormGroup> </UFormGroup>
<UFormGroup label="Icon " name="icon"> <UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" /> <USelectMenu
</UFormGroup> v-model="state.icon"
:loading="loading"
:searchable="search"
placeholder="Select an icon"
searchable-placeholder="Search an icon"
>
<template #label>
<div v-if="state.icon" class="flex items-center gap-2">
<UIcon size="20" :name="`i-${state.icon}`" />
<span class="truncate">{{ state.icon }}</span>
</div>
</template>
<template #option="{ option }">
<div class="flex items-center gap-2">
<UIcon size="20" :name="`i-${option}`" />
<span class="truncate">{{ option }}</span>
</div>
</template>
<template #empty>
Enter an icon name, keyword or tag
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup> <UFormGroup>
<UCheckbox v-model="state.nameVisible" :color="state.color" label="Is the category name visible?" /> <UCheckbox v-model="state.nameVisible" :color="state.color" label="Is the category name visible?" />
</UFormGroup> </UFormGroup>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormSubmitEvent } from '#ui/types' import type { FormSubmitEvent } from '#ui/types'
import type { COLORS, TabType, UpdateTabSchemaType } from '~~/types/types' import type { TabType, UpdateTabSchemaType } from '~~/types/types'
import { UpdateTabSchema } from '~~/types/types' import { COLORS, UpdateTabSchema } from '~~/types/types'
const props = defineProps<{ const props = defineProps<{
tab: TabType | null tab: TabType | null
@@ -10,13 +10,14 @@ const props = defineProps<{
const emit = defineEmits(['closeModal']) const emit = defineEmits(['closeModal'])
const { categories } = await useCategories() const { categories } = await useCategories()
const { updateTab } = await useTabs() const { updateTab } = await useTabs()
const { loading, search } = useIcons()
const state = reactive({ const state = reactive({
name: props.tab?.name, name: undefined,
icon: props.tab?.icon, icon: undefined,
color: props.tab?.color, color: COLORS[0],
primary: props.tab?.primary, primary: undefined,
categoryId: props.tab?.categoryId, categoryId: undefined,
}) })
watchEffect(() => { watchEffect(() => {
@@ -65,7 +66,31 @@ async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
</UFormGroup> </UFormGroup>
<UFormGroup label="Icon " name="icon"> <UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" /> <USelectMenu
v-model="state.icon"
:loading="loading"
:searchable="search"
placeholder="Select an icon"
searchable-placeholder="Search an icon"
>
<template #label>
<div v-if="state.icon" class="flex items-center gap-2">
<UIcon size="20" :name="`i-${state.icon}`" />
<span class="truncate">{{ state.icon }}</span>
</div>
</template>
<template #option="{ option }">
<div class="flex items-center gap-2">
<UIcon size="20" :name="`i-${option}`" />
<span class="truncate">{{ option }}</span>
</div>
</template>
<template #empty>
Enter an icon name, keyword or tag
</template>
</USelectMenu>
</UFormGroup> </UFormGroup>
<UFormGroup label="Category " name="category"> <UFormGroup label="Category " name="category">

View File

@@ -1,55 +0,0 @@
<script setup lang="ts">
import type { AppType } from '~~/types/types'
defineProps<{
title: string
apps: AppType[]
}>()
</script>
<template>
<section>
<h1 class="font-bold text-xl mb-4">
{{ title }}
</h1>
<div v-if="apps" class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 md:grid-cols-5 gap-4">
<ULink v-for="app in apps" :key="app.name" :to="app.url" class="relative" target="_blank">
<div v-show="app.primary === true" class="absolute flex h-4 w-4 -right-2 -top-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span class="relative inline-flex rounded-full h-4 w-4 bg-green-500" />
</div>
<UCard
:ui="{ body: { base: 'h-full' }, background: 'h-full duration-300 bg-white hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800' }"
>
<div class="flex gap-4 items-center h-full">
<UBadge :color="app.color" class="p-2" variant="soft">
<UIcon :name="app.icon" size="32" />
</UBadge>
<div class="flex flex-col gap-1">
<div v-if="app.nuxt" class="text-xl flex gap-1 items-center">
<div class="flex">
<p>{{ app.name }}</p>
<p :class="`text-${app.color}-400 font-medium`">
{{ app.nuxt }}
</p>
</div>
</div>
<div v-else :class="`text-${app.color}-400`" class="text-xl font-medium">
<p>{{ app.name }}</p>
</div>
<div class="flex gap-2 mt-1">
<UBadge
v-for="tag in app.tags"
:key="tag.name"
:color="tag.color"
:label="tag.name"
variant="soft"
/>
</div>
</div>
</div>
</UCard>
</ULink>
</div>
</section>
</template>

View File

@@ -4,7 +4,7 @@ export async function useCategories() {
const { data: categories, refresh } const { data: categories, refresh }
= await useAsyncData<CategoryType[]>(async () => await useRequestFetch()('/api/categories')) = await useAsyncData<CategoryType[]>(async () => await useRequestFetch()('/api/categories'))
async function getCategory(id: number) { async function getCategory(id: number): CategoryType {
return categories.data.value.find(category => category.id === id) return categories.data.value.find(category => category.id === id)
} }

18
app/composables/icons.ts Normal file
View File

@@ -0,0 +1,18 @@
export function useIcons() {
const loading = ref(false)
async function search(query: string) {
if (query) {
loading.value = true
}
const response = await $fetch('/api/icons/search', {
query: { query },
})
loading.value = false
return response.icons
}
return {
loading,
search,
}
}

View File

@@ -1,4 +1,4 @@
import type { CreateTabSchema, TabType, UpdateTabSchema } from '~~/types/types' import type { CreateTabSchema, type TabType, UpdateTabSchema } from '~~/types/types'
export async function useTabs() { export async function useTabs() {
const { data: tabs, refresh } const { data: tabs, refresh }
@@ -32,6 +32,21 @@ export async function useTabs() {
.catch(error => useErrorToast('Tab update failed!', `Error: ${error}`)) .catch(error => useErrorToast('Tab update failed!', `Error: ${error}`))
} }
async function setTabPrimary(tab, primary: boolean) {
await $fetch(`/api/tabs/${tab.id}`, {
method: 'PUT',
body: JSON.stringify({
primary,
categoryId: tab.categoryId,
}),
})
.then(async () => {
await refresh()
useSuccessToast('Tab favorite toggled with success!')
})
.catch(error => useErrorToast('Cannot toggle Tab favorite!', `Error: ${error}`))
}
async function deleteTab(id: number) { async function deleteTab(id: number) {
await $fetch(`/api/tabs/${id}`, { await $fetch(`/api/tabs/${id}`, {
method: 'DELETE', method: 'DELETE',
@@ -47,5 +62,6 @@ export async function useTabs() {
deleteTab, deleteTab,
getTabsForCategory, getTabsForCategory,
updateTab, updateTab,
setTabPrimary,
} }
} }

View File

@@ -1,22 +1,26 @@
export function useUserLimit() { export async function useUserLimit() {
function hasUserFreePlan() { const { user } = useUserSession()
return true const { categories } = await useCategories()
const { tabs } = await useTabs()
const hasPaidPlan = computed(() => user.value.subscription !== 'free')
function canCreateCategory() {
if (hasPaidPlan.value)
return true
return categories.value.length < 3
} }
function getRemainingCategories() { function canCreateTabInCategory(categoryId: number): boolean {
if (!hasUserFreePlan()) if (hasPaidPlan.value)
return -1 return true
return 3 return tabs.filter(tab => tab.categoryId === categoryId).length < 5
}
function getRemainingTabs(category_id: number) {
if (!hasUserFreePlan())
return -1
return category_id * 3
} }
return { return {
getRemainingCategories, hasPaidPlan,
getRemainingTabs, userLimits,
canCreateCategory,
canCreateTabInCategory,
} }
} }

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CategoryType } from '~~/types/types' import type { CategoryType } from '~~/types/types'
import CategoryHeader from '~/components/CategoryHeader.vue'
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',
@@ -14,6 +13,7 @@ onMounted(() => {
const { user } = useUserSession() const { user } = useUserSession()
const { categories } = await useCategories() const { categories } = await useCategories()
const { getTabsForCategory } = await useTabs() const { getTabsForCategory } = await useTabs()
const { canCreateCategory } = await useUserLimit()
// Modals // Modals
const createCategoryModal = ref(false) const createCategoryModal = ref(false)
@@ -59,7 +59,11 @@ const items = [[
]] ]]
defineShortcuts({ defineShortcuts({
c: () => createCategoryModal.value = true, c: () => {
if (canCreateCategory()) {
createCategoryModal.value = true
}
},
}) })
</script> </script>
@@ -77,6 +81,7 @@ defineShortcuts({
</div> </div>
<div class="flex justify-end mb-8 gap-4"> <div class="flex justify-end mb-8 gap-4">
<UButton <UButton
v-if="canCreateCategory()"
icon="i-ph:folder-simple-plus-duotone" icon="i-ph:folder-simple-plus-duotone"
color="black" color="black"
variant="solid" variant="solid"
@@ -86,6 +91,19 @@ defineShortcuts({
Create Category Create Category
<UKbd>C</UKbd> <UKbd>C</UKbd>
</UButton> </UButton>
<UTooltip v-else text="You can't create more categories on free plan. ❌">
<UButton
icon="i-ph:folder-simple-plus-duotone"
color="black"
variant="solid"
size="lg"
disabled
@click.prevent="createCategoryModal = true"
>
Create Category
<UKbd>C</UKbd>
</UButton>
</UTooltip>
</div> </div>
<section v-if="categories"> <section v-if="categories">
<div v-if="categories.length > 0" class="space-y-12"> <div v-if="categories.length > 0" class="space-y-12">
@@ -93,12 +111,12 @@ defineShortcuts({
v-for="category in categories" v-for="category in categories"
:key="category.id" :key="category.id"
> >
<CategoryHeader <AppCategory
:dropdown-items="items" :dropdown-items="items"
:category="category" :category="category"
@create-tab="openCreateTab(category)" @create-tab="openCreateTab(category)"
/> />
<div v-if="getTabsForCategory(category.id).length > 0" class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 md:grid-cols-5 gap-4"> <div v-if="getTabsForCategory(category.id).length > 0" class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 md:grid-cols-4 gap-4">
<AppTab <AppTab
v-for="tab in getTabsForCategory(category.id)" v-for="tab in getTabsForCategory(category.id)"
:key="tab.id" :key="tab.id"

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
const { user, loggedIn, clear } = useUserSession()
</script>
<template>
<div>
<div>
User: {{ user }}
</div>
<div>
LoggedIn: {{ loggedIn }}
</div>
<div @click="clear">
clear
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,16 @@
export default defineEventHandler(async (event) => {
const collections = ['ph', 'heroicons']
const { query } = getQuery(event)
const response = await $fetch('https://api.iconify.design/search', {
params: {
query,
prefixes: collections.join(','),
},
})
return {
total: response.total,
icons: response.icons && response.icons.length > 0 ? response.icons.slice(0, 25) : response.icons,
}
})

View File

@@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
name: body.name, name: body.name,
icon: body.icon, icon: body.icon,
color: body.color, color: body.color,
nameVisible: body.nameVisible, primary: body.primary,
link: body.link, link: body.link,
}) })
.where( .where(

View File

@@ -8,5 +8,5 @@ export const tables = schema
export function useDrizzle() { export function useDrizzle() {
const config = useRuntimeConfig() const config = useRuntimeConfig()
return drizzle(postgres(config.postgres.url, { prepare: false }), { schema }) return drizzle(postgres(config.postgres.url, { prepare: false, max: 50 }), { schema })
} }