feat: improvements on ui

This commit is contained in:
Paulo Castellano 2026-01-21 18:42:37 -03:00
parent 9f67f4042d
commit 1b7e161daa
58 changed files with 331 additions and 277 deletions

10
package-lock.json generated
View file

@ -16,7 +16,6 @@
"embla-carousel-vue": "^8.6.0",
"laravel-vite-plugin": "^2.0.0",
"laravel-vue-i18n": "^2.8.0",
"lucide-vue-next": "^0.468.0",
"maska": "^3.2.0",
"reka-ui": "^2.7.0",
"tailwind-merge": "^3.2.0",
@ -5439,15 +5438,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lucide-vue-next": {
"version": "0.468.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz",
"integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View file

@ -47,7 +47,6 @@
"embla-carousel-vue": "^8.6.0",
"laravel-vite-plugin": "^2.0.0",
"laravel-vue-i18n": "^2.8.0",
"lucide-vue-next": "^0.468.0",
"maska": "^3.2.0",
"reka-ui": "^2.7.0",
"tailwind-merge": "^3.2.0",

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { AlertCircle } from 'lucide-vue-next';
import { IconAlertCircle } from '@tabler/icons-vue';
import { computed } from 'vue';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
@ -18,7 +18,7 @@ const uniqueErrors = computed(() => Array.from(new Set(props.errors)));
<template>
<Alert variant="destructive">
<AlertCircle class="size-4" />
<IconAlertCircle class="size-4" />
<AlertTitle>{{ title }}</AlertTitle>
<AlertDescription>
<ul class="list-inside list-disc text-sm">

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { InertiaLinkProps } from '@inertiajs/vue3';
import { Link, usePage } from '@inertiajs/vue3';
import { Calendar, Menu, Settings, Share2, Users } from 'lucide-vue-next';
import { IconCalendar, IconMenu2, IconSettings, IconShare, IconUsers } from '@tabler/icons-vue';
import { computed } from 'vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
@ -55,22 +55,22 @@ const mainNavItems: NavItem[] = [
{
title: 'Calendar',
href: '/calendar',
icon: Calendar,
icon: IconCalendar,
},
{
title: 'Accounts',
href: '/accounts',
icon: Share2,
icon: IconShare,
},
{
title: 'Members',
href: '/members',
icon: Users,
icon: IconUsers,
},
{
title: 'Settings',
href: '/settings',
icon: Settings,
icon: IconSettings,
},
];
</script>
@ -84,7 +84,7 @@ const mainNavItems: NavItem[] = [
<Sheet>
<SheetTrigger :as-child="true">
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
<Menu class="h-5 w-5" />
<IconMenu2 class="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="w-[300px] p-6">

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link, router, usePage } from '@inertiajs/vue3';
import { IconCalendar, IconCheck, IconSelector, IconFileText, IconHash, IconLogout, IconPlus, IconSettings, IconShare2, IconPencil, IconFileCheck, IconTag, IconUser, IconClock, IconMessageCircle, IconBell, IconBook, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-vue';
import { IconBrandDiscord, IconCalendar, IconCheck, IconSelector, IconFileText, IconHash, IconLogout, IconPlus, IconSettings, IconAffiliate, IconPencil, IconFileCheck, IconTag, IconUser, IconClock, IconMessageCircle, IconBell, IconBook, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-vue';
import { Button } from '@/components/ui/button';
import { create as createPost } from '@/actions/App/Http/Controllers/PostController';
import { computed } from 'vue';
@ -102,7 +102,7 @@ const configNavItems = computed(() => {
{
title: 'Connections',
href: accounts.url(),
icon: IconShare2,
icon: IconAffiliate,
},
{
title: 'Hashtags',
@ -128,6 +128,11 @@ const configNavItems = computed(() => {
});
const supportNavItems: NavItem[] = [
{
title: 'Discord',
href: 'https://trypost.it/discord',
icon: IconBrandDiscord,
},
{
title: 'Share feedback',
href: 'https://github.com/trypost-it/trypost/discussions',
@ -156,7 +161,8 @@ function createWorkspace() {
}
function isActive(href: string): boolean {
return page.url.startsWith(href);
// Exact match or match with query string
return page.url === href || page.url.startsWith(href + '?');
}
function handleLogout() {
@ -203,7 +209,7 @@ function handleLogout() {
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ auth.user.name }}</span>
<span class="truncate text-xs text-muted-foreground">{{ auth.user.email
}}</span>
}}</span>
</div>
</div>
</DropdownMenuLabel>

View file

@ -174,27 +174,15 @@ const displayText = computed(() => {
<template>
<Popover v-model:open="popoverOpen">
<PopoverTrigger as-child :disabled="disabled">
<Button
:id="name"
type="button"
variant="outline"
class="justify-between text-left font-normal"
:class="{ 'text-muted-foreground': !displayText }"
:disabled="disabled"
>
<Button :id="name" type="button" variant="outline" class="justify-between text-left font-normal"
:class="{ 'text-muted-foreground': !displayText }" :disabled="disabled">
<span>{{ displayText || placeholder }}</span>
<IconCalendar class="ml-2 h-4 w-4" />
<IconCalendar class="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" :align="align">
<Calendar
v-model="internalDate"
:placeholder="internalDate"
layout="month-and-year"
locale="en-US"
calendar-label="Date picker"
initial-focus
/>
<Calendar v-model="internalDate" :placeholder="internalDate" layout="month-and-year" locale="en-US"
calendar-label="Date picker" initial-focus />
<!-- Time Picker -->
<div v-if="showTime" class="border-t p-3">
<div class="flex items-center gap-2">
@ -223,4 +211,4 @@ const displayText = computed(() => {
</div>
</PopoverContent>
</Popover>
</template>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import * as icons from 'lucide-vue-next';
import * as icons from '@tabler/icons-vue';
import { computed } from 'vue';
import { cn } from '@/lib/utils';
@ -21,7 +21,8 @@ const props = withDefaults(defineProps<Props>(), {
const className = computed(() => cn('h-4 w-4', props.class));
const icon = computed(() => {
const iconName = props.name.charAt(0).toUpperCase() + props.name.slice(1);
// Tabler icons are prefixed with 'Icon', e.g., 'IconUser', 'IconSettings'
const iconName = 'Icon' + props.name.charAt(0).toUpperCase() + props.name.slice(1);
return (icons as Record<string, any>)[iconName];
});
</script>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { ChevronsUpDown } from 'lucide-vue-next';
import { IconSelector } from '@tabler/icons-vue';
import {
DropdownMenu,
@ -33,7 +33,7 @@ const { isMobile, state } = useSidebar();
data-test="sidebar-menu-button"
>
<UserInfo :user="user" />
<ChevronsUpDown class="ml-auto size-4" />
<IconSelector class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import axios from 'axios';
import { Trash2, Upload, User } from 'lucide-vue-next';
import { IconTrash, IconUpload, IconUser } from '@tabler/icons-vue';
import { ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
@ -164,7 +164,7 @@ const closeDialog = () => {
roundedClasses[rounded],
]"
>
<User class="h-8 w-8 text-muted-foreground" />
<IconUser class="h-8 w-8 text-muted-foreground" />
</div>
</div>
@ -188,7 +188,7 @@ const closeDialog = () => {
:disabled="uploading"
@click="openDialog"
>
<Upload class="h-4 w-4" />
<IconUpload class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
@ -213,7 +213,7 @@ const closeDialog = () => {
class="px-2"
@click="removePhoto"
>
<Trash2 class="h-4 w-4" />
<IconTrash class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
@ -232,7 +232,7 @@ const closeDialog = () => {
<div class="flex flex-col items-center gap-4 py-8">
<div class="rounded-full bg-muted p-6">
<Upload class="h-12 w-12 text-muted-foreground" />
<IconUpload class="h-12 w-12 text-muted-foreground" />
</div>
<p class="text-center text-sm text-muted-foreground">
Select an image from your computer

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { AlertCircle, Check, ExternalLink, RefreshCw, Trash2 } from 'lucide-vue-next';
import { IconAlertCircle, IconCheck, IconExternalLink, IconRefresh, IconTrash } from '@tabler/icons-vue';
import { computed, onMounted, onUnmounted } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
@ -154,13 +154,13 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
v-if="platform.connected && !isDisconnected(platform.account)"
class="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-green-500 text-white ring-2 ring-white dark:ring-gray-900"
>
<Check class="h-3 w-3" />
<IconCheck class="h-3 w-3" />
</div>
<div
v-else-if="platform.connected && isDisconnected(platform.account)"
class="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-white ring-2 ring-white dark:ring-gray-900"
>
<AlertCircle class="h-3 w-3" />
<IconAlertCircle class="h-3 w-3" />
</div>
</div>
<div class="flex-1 min-w-0">
@ -180,7 +180,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
<div v-if="platform.connected && platform.account" class="border-t px-4 py-3">
<!-- Disconnected Warning -->
<div v-if="isDisconnected(platform.account)" class="mb-3 flex items-start gap-2 rounded-lg bg-red-100 p-2 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
<AlertCircle class="h-4 w-4 mt-0.5 shrink-0" />
<IconAlertCircle class="h-4 w-4 mt-0.5 shrink-0" />
<div class="flex-1 min-w-0">
<p class="font-medium">Connection lost</p>
<p v-if="platform.account.error_message" class="text-xs truncate opacity-80">
@ -210,7 +210,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
@click="openOAuthPopup(platform.value)"
class="p-2 text-amber-600 hover:text-amber-700 transition-colors"
>
<RefreshCw class="h-4 w-4" />
<IconRefresh class="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>
@ -226,7 +226,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
target="_blank"
class="p-2 text-muted-foreground hover:text-foreground transition-colors"
>
<ExternalLink class="h-4 w-4" />
<IconExternalLink class="h-4 w-4" />
</a>
</TooltipTrigger>
<TooltipContent>
@ -241,7 +241,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
@click="emit('disconnect', platform.account.id)"
class="p-2 text-muted-foreground hover:text-red-600 transition-colors"
>
<Trash2 class="h-4 w-4" />
<IconTrash class="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link, router } from '@inertiajs/vue3';
import { LogOut, Settings } from 'lucide-vue-next';
import { IconLogout, IconSettings } from '@tabler/icons-vue';
import {
DropdownMenuGroup,
@ -34,7 +34,7 @@ defineProps<Props>();
<DropdownMenuGroup>
<DropdownMenuItem :as-child="true">
<Link class="block w-full cursor-pointer" :href="edit()" prefetch>
<Settings class="mr-2 h-4 w-4" />
<IconSettings class="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
@ -48,7 +48,7 @@ defineProps<Props>();
as="button"
data-test="logout-button"
>
<LogOut class="mr-2 h-4 w-4" />
<IconLogout class="mr-2 h-4 w-4" />
Log out
</Link>
</DropdownMenuItem>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { router, usePage } from '@inertiajs/vue3';
import { Check, ChevronsUpDown, Plus } from 'lucide-vue-next';
import { IconCheck, IconPlus, IconSelector } from '@tabler/icons-vue';
import { computed } from 'vue';
import { Button } from '@/components/ui/button';
@ -39,7 +39,7 @@ function createWorkspace() {
<DropdownMenuTrigger :as-child="true">
<Button variant="outline" class="w-full justify-between gap-2 px-3" :class="{ 'text-muted-foreground': !currentWorkspace }">
<span class="truncate">{{ currentWorkspace?.name || 'Select workspace' }}</span>
<ChevronsUpDown class="h-4 w-4 shrink-0 opacity-50" />
<IconSelector class="h-4 w-4 shrink-0 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" class="w-56">
@ -53,12 +53,12 @@ function createWorkspace() {
>
<div class="flex w-full items-center justify-between">
<span class="truncate">{{ workspace.name }}</span>
<Check v-if="currentWorkspace?.id === workspace.id" class="h-4 w-4 shrink-0" />
<IconCheck v-if="currentWorkspace?.id === workspace.id" class="h-4 w-4 shrink-0" />
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem class="cursor-pointer" @click="createWorkspace">
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
Create workspace
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { MoreHorizontal } from "lucide-vue-next"
import { IconDots } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
@ -16,7 +16,7 @@ const props = defineProps<{
:class="cn('flex size-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="size-4" />
<IconDots class="size-4" />
</slot>
<span class="sr-only">More</span>
</span>

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { ChevronRight } from "lucide-vue-next"
import { IconChevronRight } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
@ -16,7 +16,7 @@ const props = defineProps<{
:class="cn('[&>svg]:size-3.5', props.class)"
>
<slot>
<ChevronRight />
<IconChevronRight />
</slot>
</li>
</template>

View file

@ -2,7 +2,7 @@
import type { CalendarNextProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import { IconChevronRight } from "@tabler/icons-vue"
import { CalendarNext, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
@ -25,7 +25,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="size-4" />
<IconChevronRight class="size-4" />
</slot>
</CalendarNext>
</template>

View file

@ -2,7 +2,7 @@
import type { CalendarPrevProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronLeft } from "lucide-vue-next"
import { IconChevronLeft } from "@tabler/icons-vue"
import { CalendarPrev, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
@ -25,7 +25,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="size-4" />
<IconChevronLeft class="size-4" />
</slot>
</CalendarPrev>
</template>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { WithClassAsProps } from "./interface"
import type { ButtonVariants } from '@/components/ui/button'
import { ArrowRight } from "lucide-vue-next"
import { IconArrowRight } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'
import { useCarousel } from "./useCarousel"
@ -34,7 +34,7 @@ const { orientation, canScrollNext, scrollNext } = useCarousel()
@click="scrollNext"
>
<slot>
<ArrowRight />
<IconArrowRight />
<span class="sr-only">Next Slide</span>
</slot>
</Button>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { WithClassAsProps } from "./interface"
import type { ButtonVariants } from '@/components/ui/button'
import { ArrowLeft } from "lucide-vue-next"
import { IconArrowLeft } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'
import { useCarousel } from "./useCarousel"
@ -34,7 +34,7 @@ const { orientation, canScrollPrev, scrollPrev } = useCarousel()
@click="scrollPrev"
>
<slot>
<ArrowLeft />
<IconArrowLeft />
<span class="sr-only">Previous Slide</span>
</slot>
</Button>

View file

@ -2,7 +2,7 @@
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { IconCheck } from "@tabler/icons-vue"
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
@ -28,7 +28,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
class="grid place-content-center text-current transition-none"
>
<slot v-bind="slotProps">
<Check class="size-3.5" />
<IconCheck class="size-3.5" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>

View file

@ -2,7 +2,7 @@
import type { ListboxFilterProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Search } from "lucide-vue-next"
import { IconSearch } from "@tabler/icons-vue"
import { ListboxFilter, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { useCommand } from "."
@ -27,7 +27,7 @@ const { filterState } = useCommand()
data-slot="command-input-wrapper"
class="flex h-9 items-center gap-2 border-b px-3"
>
<Search class="size-4 shrink-0 opacity-50" />
<IconSearch class="size-4 shrink-0 opacity-50" />
<ListboxFilter
v-bind="{ ...forwardedProps, ...$attrs }"
v-model="filterState.search"

View file

@ -2,7 +2,7 @@
import type { ContextMenuCheckboxItemEmits, ContextMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { IconCheck } from "@tabler/icons-vue"
import {
ContextMenuCheckboxItem,
ContextMenuItemIndicator,
@ -30,7 +30,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
<IconCheck class="size-4" />
</slot>
</ContextMenuItemIndicator>
</span>

View file

@ -2,7 +2,7 @@
import type { ContextMenuRadioItemEmits, ContextMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Circle } from "lucide-vue-next"
import { IconCircleFilled } from "@tabler/icons-vue"
import {
ContextMenuItemIndicator,
ContextMenuRadioItem,
@ -30,7 +30,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
<IconCircleFilled class="size-2" />
</slot>
</ContextMenuItemIndicator>
</span>

View file

@ -2,7 +2,7 @@
import type { ContextMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import { IconChevronRight } from "@tabler/icons-vue"
import {
ContextMenuSubTrigger,
useForwardProps,
@ -27,6 +27,6 @@ const forwardedProps = useForwardProps(delegatedProps)
)"
>
<slot />
<ChevronRight class="ml-auto" />
<IconChevronRight class="ml-auto" />
</ContextMenuSubTrigger>
</template>

View file

@ -2,7 +2,7 @@
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { IconX } from "@tabler/icons-vue"
import {
DialogClose,
DialogContent,
@ -45,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
data-slot="dialog-close"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<IconX />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View file

@ -2,7 +2,7 @@
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { IconX } from "@tabler/icons-vue"
import {
DialogClose,
DialogContent,
@ -50,7 +50,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="w-4 h-4" />
<IconX class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View file

@ -2,7 +2,7 @@
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { IconCheck } from "@tabler/icons-vue"
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
@ -30,7 +30,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
<IconCheck class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>

View file

@ -2,7 +2,7 @@
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Circle } from "lucide-vue-next"
import { IconCircleFilled } from "@tabler/icons-vue"
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
<IconCircleFilled class="size-2" />
</slot>
</DropdownMenuItemIndicator>
</span>

View file

@ -2,7 +2,7 @@
import type { DropdownMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import { IconChevronRight } from "@tabler/icons-vue"
import {
DropdownMenuSubTrigger,
useForwardProps,
@ -25,6 +25,6 @@ const forwardedProps = useForwardProps(delegatedProps)
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
<IconChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { MinusIcon } from "lucide-vue-next"
import { IconMinus } from "@tabler/icons-vue"
import { useForwardProps } from "reka-ui"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
@ -15,7 +15,7 @@ const forwarded = useForwardProps(props)
v-bind="forwarded"
>
<slot>
<MinusIcon />
<IconMinus />
</slot>
</div>
</template>

View file

@ -2,7 +2,7 @@
import type { AcceptableValue } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit, useVModel } from "@vueuse/core"
import { ChevronDownIcon } from "lucide-vue-next"
import { IconChevronDown } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
defineOptions({
@ -41,7 +41,7 @@ const delegatedProps = reactiveOmit(props, "class")
>
<slot />
</select>
<ChevronDownIcon
<IconChevronDown
class="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
aria-hidden="true"
data-slot="native-select-icon"

View file

@ -2,7 +2,7 @@
import type { NavigationMenuTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { IconChevronDown } from "@tabler/icons-vue"
import {
NavigationMenuTrigger,
useForwardProps,
@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
:class="cn(navigationMenuTriggerStyle(), 'group', props.class)"
>
<slot />
<ChevronDown
<IconChevronDown
class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>

View file

@ -2,7 +2,7 @@
import type { RadioGroupItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { CircleIcon } from "lucide-vue-next"
import { IconCircleFilled } from "@tabler/icons-vue"
import {
RadioGroupIndicator,
RadioGroupItem,
@ -33,7 +33,7 @@ const forwardedProps = useForwardProps(delegatedProps)
class="relative flex items-center justify-center"
>
<slot>
<CircleIcon class="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
<IconCircleFilled class="text-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</slot>
</RadioGroupIndicator>
</RadioGroupItem>

View file

@ -2,7 +2,7 @@
import type { RangeCalendarNextProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import { IconChevronRight } from "@tabler/icons-vue"
import { RangeCalendarNext, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
@ -26,7 +26,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="size-4" />
<IconChevronRight class="size-4" />
</slot>
</RangeCalendarNext>
</template>

View file

@ -2,7 +2,7 @@
import type { RangeCalendarPrevProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronLeft } from "lucide-vue-next"
import { IconChevronLeft } from "@tabler/icons-vue"
import { RangeCalendarPrev, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
@ -26,7 +26,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="size-4" />
<IconChevronLeft class="size-4" />
</slot>
</RangeCalendarPrev>
</template>

View file

@ -2,7 +2,7 @@
import type { SplitterResizeHandleEmits, SplitterResizeHandleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { GripVertical } from "lucide-vue-next"
import { IconGripVertical } from "@tabler/icons-vue"
import { SplitterResizeHandle, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
@ -22,7 +22,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template v-if="props.withHandle">
<div class="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<slot>
<GripVertical class="size-2.5" />
<IconGripVertical class="size-2.5" />
</slot>
</div>
</template>

View file

@ -2,7 +2,7 @@
import type { SelectItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { IconCheck } from "@tabler/icons-vue"
import {
SelectItem,
SelectItemIndicator,
@ -32,7 +32,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<span class="absolute right-2 flex size-3.5 items-center justify-center">
<SelectItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
<IconCheck class="size-4" />
</slot>
</SelectItemIndicator>
</span>

View file

@ -2,7 +2,7 @@
import type { SelectScrollDownButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { IconChevronDown } from "@tabler/icons-vue"
import { SelectScrollDownButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronDown class="size-4" />
<IconChevronDown class="size-4" />
</slot>
</SelectScrollDownButton>
</template>

View file

@ -2,7 +2,7 @@
import type { SelectScrollUpButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronUp } from "lucide-vue-next"
import { IconChevronUp } from "@tabler/icons-vue"
import { SelectScrollUpButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronUp class="size-4" />
<IconChevronUp class="size-4" />
</slot>
</SelectScrollUpButton>
</template>

View file

@ -2,7 +2,7 @@
import type { SelectTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { IconChevronDown } from "@tabler/icons-vue"
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
@ -27,7 +27,7 @@ const forwardedProps = useForwardProps(delegatedProps)
>
<slot />
<SelectIcon as-child>
<ChevronDown class="size-4 opacity-50" />
<IconChevronDown class="size-4 opacity-50" />
</SelectIcon>
</SelectTrigger>
</template>

View file

@ -2,7 +2,7 @@
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { IconX } from "@tabler/icons-vue"
import {
DialogClose,
DialogContent,
@ -54,7 +54,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<X class="size-4" />
<IconX class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { PanelLeftClose, PanelLeftOpen } from "lucide-vue-next"
import { IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftExpand } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'
import { useSidebar } from "./utils"
@ -21,8 +21,8 @@ const { isMobile, state, toggleSidebar } = useSidebar()
:class="cn('h-7 w-7', props.class)"
@click="toggleSidebar"
>
<PanelLeftOpen v-if="isMobile || state === 'collapsed'" />
<PanelLeftClose v-else />
<IconLayoutSidebarLeftExpand v-if="isMobile || state === 'collapsed'" />
<IconLayoutSidebarLeftCollapse v-else />
<span class="sr-only">Toggle Sidebar</span>
</Button>
</template>

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ToasterProps } from "vue-sonner"
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from "lucide-vue-next"
import { IconCircleCheck, IconInfoCircle, IconLoader2, IconAlertOctagon, IconAlertTriangle, IconX } from "@tabler/icons-vue"
import { Toaster as Sonner } from "vue-sonner"
import { cn } from "@/lib/utils"
@ -19,24 +19,24 @@ const props = defineProps<ToasterProps>()
v-bind="props"
>
<template #success-icon>
<CircleCheckIcon class="size-4" />
<IconCircleCheck class="size-4" />
</template>
<template #info-icon>
<InfoIcon class="size-4" />
<IconInfoCircle class="size-4" />
</template>
<template #warning-icon>
<TriangleAlertIcon class="size-4" />
<IconAlertTriangle class="size-4" />
</template>
<template #error-icon>
<OctagonXIcon class="size-4" />
<IconAlertOctagon class="size-4" />
</template>
<template #loading-icon>
<div>
<Loader2Icon class="size-4 animate-spin" />
<IconLoader2 class="size-4 animate-spin" />
</div>
</template>
<template #close-icon>
<XIcon class="size-4" />
<IconX class="size-4" />
</template>
</Sonner>
</template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { Loader2Icon } from "lucide-vue-next"
import { IconLoader2 } from "@tabler/icons-vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
@ -9,7 +9,7 @@ const props = defineProps<{
</script>
<template>
<Loader2Icon
<IconLoader2
role="status"
aria-label="Loading"
:class="cn('size-4 animate-spin', props.class)"

View file

@ -2,7 +2,7 @@
import type { TagsInputItemDeleteProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { IconX } from "@tabler/icons-vue"
import { TagsInputItemDelete, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<template>
<TagsInputItemDelete v-bind="forwardedProps" :class="cn('flex rounded bg-transparent mr-1', props.class)">
<slot>
<X class="w-4 h-4" />
<IconX class="w-4 h-4" />
</slot>
</TagsInputItemDelete>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Facebook, Check } from 'lucide-vue-next';
import { IconBrandFacebook, IconCheck } from '@tabler/icons-vue';
import { ref } from 'vue';
import { Alert, AlertDescription } from '@/components/ui/alert';
@ -62,7 +62,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
<div v-if="pages.length === 0 && !error" class="text-center py-12">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-muted">
<Facebook class="h-7 w-7 text-muted-foreground" />
<IconBrandFacebook class="h-7 w-7 text-muted-foreground" />
</div>
<h3 class="mt-4 text-lg font-semibold">No pages found</h3>
<p class="mt-1 text-sm text-muted-foreground">
@ -81,7 +81,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
<Avatar class="h-12 w-12 rounded-lg">
<AvatarImage v-if="page.picture" :src="page.picture" class="object-cover" />
<AvatarFallback class="rounded-lg bg-blue-100 dark:bg-blue-900">
<Facebook class="h-6 w-6 text-blue-600 dark:text-blue-400" />
<IconBrandFacebook class="h-6 w-6 text-blue-600 dark:text-blue-400" />
</AvatarFallback>
</Avatar>
<div class="flex-1 min-w-0">
@ -97,7 +97,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground"
>
<Check class="h-4 w-4" />
<IconCheck class="h-4 w-4" />
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Building2, Check } from 'lucide-vue-next';
import { IconBuilding, IconCheck } from '@tabler/icons-vue';
import { ref } from 'vue';
import { Alert, AlertDescription } from '@/components/ui/alert';
@ -65,7 +65,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
<div v-if="organizations.length === 0 && !error" class="text-center py-12">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-muted">
<Building2 class="h-7 w-7 text-muted-foreground" />
<IconBuilding class="h-7 w-7 text-muted-foreground" />
</div>
<h3 class="mt-4 text-lg font-semibold">No pages found</h3>
<p class="mt-1 text-sm text-muted-foreground">
@ -84,7 +84,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
<Avatar class="h-12 w-12 rounded-lg">
<AvatarImage v-if="org.logo" :src="org.logo" class="object-cover" />
<AvatarFallback class="rounded-lg bg-blue-100 dark:bg-blue-900">
<Building2 class="h-6 w-6 text-blue-600 dark:text-blue-400" />
<IconBuilding class="h-6 w-6 text-blue-600 dark:text-blue-400" />
</AvatarFallback>
</Avatar>
<div class="flex-1 min-w-0">
@ -100,7 +100,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground"
>
<Check class="h-4 w-4" />
<IconCheck class="h-4 w-4" />
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { CreditCard, FileText, Building2, ExternalLink, Sparkles } from 'lucide-vue-next';
import { IconCreditCard, IconFileText, IconBuilding, IconExternalLink, IconSparkles } from '@tabler/icons-vue';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@ -86,7 +86,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
</div>
<Alert v-if="onTrial" class="border-primary/50 bg-primary/5">
<Sparkles class="h-4 w-4 text-primary" />
<IconSparkles class="h-4 w-4 text-primary" />
<AlertTitle>Trial period active</AlertTitle>
<AlertDescription>
Your trial ends on <strong>{{ trialEndsAt }}</strong>.
@ -99,7 +99,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle class="flex items-center gap-2">
<Building2 class="h-5 w-5" />
<IconBuilding class="h-5 w-5" />
Your Subscription
</CardTitle>
<Badge :variant="getStatusVariant(subscription?.stripe_status || '')">
@ -120,7 +120,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
</div>
<div v-if="defaultPaymentMethod" class="flex items-center gap-3 p-3 border rounded-lg">
<CreditCard class="h-8 w-8 text-muted-foreground" />
<IconCreditCard class="h-8 w-8 text-muted-foreground" />
<div>
<p class="font-medium capitalize">{{ defaultPaymentMethod.brand }} **** {{ defaultPaymentMethod.last4 }}</p>
<p class="text-sm text-muted-foreground">
@ -137,7 +137,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
</CardContent>
<CardFooter>
<Button @click="openPortal" variant="outline" class="w-full">
<ExternalLink class="mr-2 h-4 w-4" />
<IconExternalLink class="mr-2 h-4 w-4" />
Manage on Stripe
</Button>
</CardFooter>
@ -146,7 +146,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<FileText class="h-5 w-5" />
<IconFileText class="h-5 w-5" />
Invoices
</CardTitle>
<CardDescription>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { useEcho } from '@laravel/echo-vue';
import { Loader2, CheckCircle, XCircle } from 'lucide-vue-next';
import { IconLoader2, IconCircleCheck, IconCircleX } from '@tabler/icons-vue';
import { ref, onMounted } from 'vue';
import { Button } from '@/components/ui/button';
@ -62,7 +62,7 @@ function retry() {
<!-- Processing -->
<template v-if="currentStatus === 'processing'">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 mb-6">
<Loader2 class="w-10 h-10 text-primary animate-spin" />
<IconLoader2 class="w-10 h-10 text-primary animate-spin" />
</div>
<h1 class="text-2xl font-bold tracking-tight mb-3">Processing your subscription</h1>
<p class="text-muted-foreground">
@ -73,7 +73,7 @@ function retry() {
<!-- Success -->
<template v-else-if="currentStatus === 'success'">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-green-100 mb-6">
<CheckCircle class="w-10 h-10 text-green-600" />
<IconCircleCheck class="w-10 h-10 text-green-600" />
</div>
<h1 class="text-2xl font-bold tracking-tight mb-3">You're all set!</h1>
<p class="text-muted-foreground">
@ -84,7 +84,7 @@ function retry() {
<!-- Cancelled -->
<template v-else-if="currentStatus === 'cancelled'">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-red-100 mb-6">
<XCircle class="w-10 h-10 text-red-600" />
<IconCircleX class="w-10 h-10 text-red-600" />
</div>
<h1 class="text-2xl font-bold tracking-tight mb-3">Checkout cancelled</h1>
<p class="text-muted-foreground mb-6">

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { Sparkles, Calendar, Users, ImageIcon, Video, Clock, BarChart3 } from 'lucide-vue-next';
import { IconSparkles, IconCalendar, IconUsers, IconPhoto, IconVideo, IconClock, IconChartBar } from '@tabler/icons-vue';
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
@ -31,12 +31,12 @@ const platforms = [
];
const features = [
{ icon: Calendar, title: 'Visual Calendar', description: 'Plan and schedule your content with an intuitive drag-and-drop calendar' },
{ icon: Clock, title: 'Unlimited Scheduling', description: 'Schedule as many posts as you want, whenever you want' },
{ icon: ImageIcon, title: 'Images & Carousels', description: 'Share single images or create engaging carousel posts' },
{ icon: Video, title: 'Video Publishing', description: 'Upload and publish videos across all your social accounts' },
{ icon: Users, title: 'Team Collaboration', description: 'Invite your team members and work together seamlessly' },
{ icon: BarChart3, title: 'Analytics', description: 'Track your post performance and engagement metrics' },
{ icon: IconCalendar, title: 'Visual Calendar', description: 'Plan and schedule your content with an intuitive drag-and-drop calendar' },
{ icon: IconClock, title: 'Unlimited Scheduling', description: 'Schedule as many posts as you want, whenever you want' },
{ icon: IconPhoto, title: 'Images & Carousels', description: 'Share single images or create engaging carousel posts' },
{ icon: IconVideo, title: 'Video Publishing', description: 'Upload and publish videos across all your social accounts' },
{ icon: IconUsers, title: 'Team Collaboration', description: 'Invite your team members and work together seamlessly' },
{ icon: IconChartBar, title: 'Analytics', description: 'Track your post performance and engagement metrics' },
];
</script>
@ -48,7 +48,7 @@ const features = [
<!-- Header -->
<div class="text-center mb-12">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-primary/10 mb-6">
<Sparkles class="w-10 h-10 text-primary" />
<IconSparkles class="w-10 h-10 text-primary" />
</div>
<h1 class="text-4xl font-bold tracking-tight mb-3">Welcome to TryPost!</h1>
<p class="text-xl text-muted-foreground">

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { Plus, Hash, Pencil, Trash2 } from 'lucide-vue-next';
import { IconPlus, IconHash, IconPencil, IconTrash } from '@tabler/icons-vue';
import { ref } from 'vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
@ -69,21 +69,21 @@ const getHashtagCount = (hashtags: string): number => {
</p>
</div>
<Button @click="isCreateDialogOpen = true">
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
New Group
</Button>
</div>
<div v-if="hashtags.length === 0" class="flex flex-col items-center justify-center py-16">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center mb-4">
<Hash class="h-8 w-8 text-muted-foreground" />
<IconHash class="h-8 w-8 text-muted-foreground" />
</div>
<h3 class="text-lg font-semibold mb-2">No hashtag groups yet</h3>
<p class="text-muted-foreground mb-4 text-center max-w-sm">
Create hashtag groups to quickly add popular hashtags to your posts
</p>
<Button @click="isCreateDialogOpen = true">
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
Create your first group
</Button>
</div>
@ -100,7 +100,7 @@ const getHashtagCount = (hashtags: string): number => {
class="h-8 w-8"
@click="openEditDialog(hashtag)"
>
<Pencil class="h-4 w-4" />
<IconPencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
@ -108,7 +108,7 @@ const getHashtagCount = (hashtags: string): number => {
class="h-8 w-8 text-destructive hover:text-destructive"
@click="handleDelete(hashtag.id)"
>
<Trash2 class="h-4 w-4" />
<IconTrash class="h-4 w-4" />
</Button>
</div>
</div>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { Plus, Tag, Pencil, Trash2 } from 'lucide-vue-next';
import { IconPlus, IconTag, IconPencil, IconTrash } from '@tabler/icons-vue';
import { ref } from 'vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
@ -65,21 +65,21 @@ const handleDelete = (labelId: string) => {
</p>
</div>
<Button @click="isCreateDialogOpen = true">
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
New Label
</Button>
</div>
<div v-if="labels.length === 0" class="flex flex-col items-center justify-center py-16">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center mb-4">
<Tag class="h-8 w-8 text-muted-foreground" />
<IconTag class="h-8 w-8 text-muted-foreground" />
</div>
<h3 class="text-lg font-semibold mb-2">No labels yet</h3>
<p class="text-muted-foreground mb-4 text-center max-w-sm">
Create labels to organize and categorize your posts
</p>
<Button @click="isCreateDialogOpen = true">
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
Create your first label
</Button>
</div>
@ -102,7 +102,7 @@ const handleDelete = (labelId: string) => {
class="h-8 w-8"
@click="openEditDialog(label)"
>
<Pencil class="h-4 w-4" />
<IconPencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
@ -110,7 +110,7 @@ const handleDelete = (labelId: string) => {
class="h-8 w-8 text-destructive hover:text-destructive"
@click="handleDelete(label.id)"
>
<Trash2 class="h-4 w-4" />
<IconTrash class="h-4 w-4" />
</Button>
</div>
</div>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { Building, Building2, Rocket, Sparkles, Store, User } from 'lucide-vue-next';
import { IconBuilding, IconRocket, IconSparkles, IconBuildingStore, IconUser } from '@tabler/icons-vue';
import { storeStep1 } from '@/actions/App/Http/Controllers/OnboardingController';
import { Button } from '@/components/ui/button';
@ -23,13 +23,13 @@ const form = useForm({
persona: '',
});
const icons: Record<string, typeof Rocket> = {
rocket: Rocket,
sparkles: Sparkles,
building: Building,
'building-2': Building2,
store: Store,
user: User,
const icons: Record<string, typeof IconRocket> = {
rocket: IconRocket,
sparkles: IconSparkles,
building: IconBuilding,
'building-2': IconBuilding,
store: IconBuildingStore,
user: IconUser,
};
const submit = () => {

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next';
import { IconChevronLeft, IconChevronRight, IconPlus } from '@tabler/icons-vue';
import { computed } from 'vue';
import { Button } from '@/components/ui/button';
@ -202,10 +202,10 @@ const formatTime = (scheduledAt: string): string => {
<div class="flex items-center gap-4">
<div class="flex items-center gap-1">
<Button variant="outline" size="icon" @click="navigate(-1)">
<ChevronLeft class="h-4 w-4" />
<IconChevronLeft class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" @click="navigate(1)">
<ChevronRight class="h-4 w-4" />
<IconChevronRight class="h-4 w-4" />
</Button>
</div>
<Button variant="outline" size="sm" @click="goToToday">
@ -224,7 +224,7 @@ const formatTime = (scheduledAt: string): string => {
</Tabs>
<Link :href="createPost.url()">
<Button>
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
New Post
</Button>
</Link>
@ -254,7 +254,7 @@ const formatTime = (scheduledAt: string): string => {
<!-- Add Post Button -->
<Link :href="createPost.url({ query: { date: day.format('YYYY-MM-DD') } })"
class="flex items-center justify-center p-2 rounded border border-dashed border-muted-foreground/30 text-muted-foreground hover:border-primary hover:text-primary hover:bg-primary/5 transition-colors">
<Plus class="h-4 w-4" />
<IconPlus class="h-4 w-4" />
</Link>
<!-- Posts -->
@ -319,7 +319,7 @@ const formatTime = (scheduledAt: string): string => {
</span>
<Link :href="createPost.url({ query: { date: day.format('YYYY-MM-DD') } })"
class="opacity-0 group-hover:opacity-100 focus:opacity-100 p-1 rounded text-muted-foreground hover:text-primary hover:bg-primary/10 transition-all">
<Plus class="h-4 w-4" />
<IconPlus class="h-4 w-4" />
</Link>
</div>

View file

@ -13,8 +13,21 @@ import {
IconBrandX,
IconBrandYoutube,
} from '@tabler/icons-vue';
import { Clock, AlertCircle, Trash2, Send, Link2, MoreHorizontal, Plus, CheckCircle, ExternalLink, Loader2 } from 'lucide-vue-next';
import { computed, ref, watch, type Component } from 'vue';
import {
IconClock,
IconAlertCircle,
IconTrash,
IconSend,
IconLink,
IconDots,
IconPlus,
IconCircleCheck,
IconExternalLink,
IconLoader2,
} from '@tabler/icons-vue';
import { computed, onUnmounted, ref, watch, type Component } from 'vue';
import debounce from '@/debounce';
import { Badge } from '@/components/ui/badge';
@ -48,7 +61,8 @@ import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
import { destroy as destroyPost, update as updatePost } from '@/routes/posts';
import { destroy as destroyPost, index as postsIndex, update as updatePost } from '@/routes/posts';
import { type BreadcrumbItemType } from '@/types';
interface SocialAccount {
id: string;
@ -118,6 +132,11 @@ interface Props {
const props = defineProps<Props>();
const breadcrumbs: BreadcrumbItemType[] = [
{ title: 'Posts', href: postsIndex.url() },
{ title: 'Edit Post', href: '#' },
];
// Reactive state for real-time updates
const post = ref(props.post);
@ -178,7 +197,12 @@ const {
postPlatforms: postPlatformsRef,
});
// Check if any platform is currently uploading
const isAnyUploading = computed(() => Object.values(isUploading.value).some(v => v));
const isSubmitting = ref(false);
const isSaving = ref(false);
const showSaved = ref(false);
const deleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
const showEnableSyncDialog = ref(false);
const showDisableSyncDialog = ref(false);
@ -263,12 +287,12 @@ const getPlatformIcon = (platform: string): Component => {
const getStatusConfig = (status: string) => {
const configs: Record<string, { label: string; color: string; icon: any }> = {
'draft': { label: 'Draft', color: 'bg-gray-100 text-gray-800', icon: Clock },
'scheduled': { label: 'Scheduled', color: 'bg-blue-100 text-blue-800', icon: Clock },
'publishing': { label: 'Publishing', color: 'bg-yellow-100 text-yellow-800', icon: Loader2 },
'published': { label: 'Published', color: 'bg-green-100 text-green-800', icon: CheckCircle },
'partially_published': { label: 'Partially Published', color: 'bg-orange-100 text-orange-800', icon: AlertCircle },
'failed': { label: 'Failed', color: 'bg-red-100 text-red-800', icon: AlertCircle },
'draft': { label: 'Draft', color: 'bg-gray-100 text-gray-800', icon: IconClock },
'scheduled': { label: 'Scheduled', color: 'bg-blue-100 text-blue-800', icon: IconClock },
'publishing': { label: 'Publishing', color: 'bg-yellow-100 text-yellow-800', icon: IconLoader2 },
'published': { label: 'Published', color: 'bg-green-100 text-green-800', icon: IconCircleCheck },
'partially_published': { label: 'Partially Published', color: 'bg-orange-100 text-orange-800', icon: IconAlertCircle },
'failed': { label: 'Failed', color: 'bg-red-100 text-red-800', icon: IconAlertCircle },
};
return configs[status] || configs['draft'];
};
@ -364,6 +388,67 @@ const getContent = (platformId: string): string => {
return platformContents.value[platformId] || '';
};
// === Autosave Logic ===
const getSubmitData = () => {
const platforms = post.value.post_platforms
.filter(pp => selectedPlatformIds.value.includes(pp.id))
.map(pp => ({
id: pp.id,
content: synced.value ? globalContent.value : platformContents.value[pp.id],
content_type: platformContentTypes.value[pp.id],
meta: platformMeta.value[pp.id] || {},
}));
return { platforms, scheduled_at: scheduledDateTime.value || null };
};
const save = () => {
if (isSubmitting.value || isReadOnly.value || isSaving.value) return;
const { platforms, scheduled_at } = getSubmitData();
isSaving.value = true;
showSaved.value = false;
router.put(updatePost.url(post.value.id), {
status: post.value.status,
synced: synced.value,
scheduled_at,
platforms,
}, {
preserveScroll: true,
onFinish: () => {
isSaving.value = false;
showSaved.value = true;
setTimeout(() => {
showSaved.value = false;
}, 2000);
},
});
};
const debouncedSave = debounce(() => {
if (!isReadOnly.value && !isAnyUploading.value && !isSubmitting.value) {
save();
}
}, 1500);
const triggerAutosave = () => {
if (!isReadOnly.value) {
showSaved.value = false;
debouncedSave();
}
};
// Watch other form data for autosave
watch([selectedPlatformIds, synced, scheduledDateTime], triggerAutosave, { deep: true });
// Cleanup on unmount
onUnmounted(() => {
debouncedSave.cancel();
});
// === Content Setters ===
// Set content for a platform
const setContent = (platformId: string, value: string) => {
if (synced.value) {
@ -375,6 +460,7 @@ const setContent = (platformId: string, value: string) => {
} else {
platformContents.value[platformId] = value;
}
triggerAutosave();
};
// Active platform
@ -398,6 +484,13 @@ const currentContentType = computed(() => {
// Set content type for a platform
const setContentType = (platformId: string, value: string) => {
platformContentTypes.value[platformId] = value;
triggerAutosave();
};
// Set meta for a platform
const setMeta = (platformId: string, value: Record<string, any>) => {
platformMeta.value[platformId] = value;
triggerAutosave();
};
// Selected platforms (for tabs)
@ -545,42 +638,12 @@ const togglePlatform = (platformId: string) => {
}
};
const getSubmitData = () => {
const platforms = post.value.post_platforms
.filter(pp => selectedPlatformIds.value.includes(pp.id))
.map(pp => ({
id: pp.id,
content: synced.value ? globalContent.value : platformContents.value[pp.id],
content_type: platformContentTypes.value[pp.id],
meta: platformMeta.value[pp.id] || {},
}));
return { platforms, scheduled_at: scheduledDateTime.value || null };
};
const save = () => {
if (isSubmitting.value || isReadOnly.value) return;
const { platforms, scheduled_at } = getSubmitData();
isSubmitting.value = true;
router.put(updatePost.url(post.value.id), {
status: post.value.status,
synced: synced.value,
scheduled_at,
platforms,
}, {
preserveScroll: true,
onFinish: () => {
isSubmitting.value = false;
},
});
};
const submit = (status: string = 'scheduled') => {
if (isSubmitting.value || isReadOnly.value) return;
// Cancel any pending autosave
debouncedSave.cancel();
const { platforms, scheduled_at } = getSubmitData();
isSubmitting.value = true;
@ -609,7 +672,7 @@ const deletePost = () => {
<Head :title="isReadOnly ? 'View Post' : 'Edit Post'" />
<AppLayout>
<AppLayout :breadcrumbs="breadcrumbs">
<div class="flex flex-col h-screen">
<!-- Top Bar with Tabs and Actions -->
<div class="relative flex items-center justify-between border-b px-4 py-2 bg-background">
@ -665,7 +728,7 @@ const deletePost = () => {
<TooltipTrigger asChild>
<button type="button" @click="showPlatformsDialog = true"
class="p-2 rounded-lg hover:bg-muted/50 transition-colors">
<MoreHorizontal class="h-5 w-5 text-muted-foreground" />
<IconDots class="h-5 w-5 text-muted-foreground" />
</button>
</TooltipTrigger>
<TooltipContent>
@ -682,7 +745,7 @@ const deletePost = () => {
<Switch id="sync-content" v-model="synced" />
</div>
<Label for="sync-content" class="text-sm cursor-pointer flex items-center gap-1.5">
<Link2 class="h-4 w-4" />
<IconLink class="h-4 w-4" />
<span>Sync</span>
<span class="flex items-center gap-1">
<component v-for="op in otherSelectedPlatforms.slice(0, 3)" :key="op.id"
@ -707,24 +770,20 @@ const deletePost = () => {
<span class="text-muted-foreground">|</span>
<Button type="button" variant="outline" size="icon-sm" @click="deletePost"
:disabled="isSaving || isSubmitting"
class="text-muted-foreground hover:text-destructive">
<Trash2 class="h-4 w-4" />
<IconTrash class="h-4 w-4" />
</Button>
<Button type="button" variant="outline" size="sm"
:disabled="selectedPlatformIds.length === 0 || isSubmitting" @click="save">
{{ isSubmitting ? 'Saving...' : 'Save' }}
</Button>
<Button type="button" variant="secondary" size="sm" :disabled="!canSubmit || isSubmitting"
<Button type="button" variant="secondary" size="sm" :disabled="!canSubmit || isSubmitting || isSaving"
@click="submit('scheduled')">
<Clock class="mr-2 h-4 w-4" />
<IconClock class="mr-2 h-4 w-4" />
Schedule
</Button>
<Button type="button" size="sm" :disabled="!canSubmit || isSubmitting"
<Button type="button" size="sm" :disabled="!canSubmit || isSubmitting || isSaving"
@click="submit('publishing')">
<Send class="mr-2 h-4 w-4" />
<IconSend class="mr-2 h-4 w-4" />
Publish
</Button>
</div>
@ -740,7 +799,18 @@ const deletePost = () => {
<div class="flex-1 overflow-hidden">
<div v-if="activePlatform && selectedPlatformIds.length > 0" class="h-full flex">
<!-- Left Side: Form -->
<div class="w-1/2 border-r overflow-y-auto">
<div class="w-1/2 border-r overflow-y-auto relative">
<!-- Autosave indicator -->
<div v-if="!isReadOnly && (isSaving || showSaved)" class="absolute top-4 left-6 z-10">
<span v-if="isSaving" class="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconLoader2 class="h-3 w-3 animate-spin" />
Saving...
</span>
<span v-else-if="showSaved" class="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconCircleCheck class="h-3 w-3 text-green-500" />
Saved
</span>
</div>
<div class="max-w-lg mx-auto py-8 px-6">
<!-- Publication Result (read-only mode) -->
<div v-if="isReadOnly" class="mb-6 space-y-3">
@ -765,14 +835,14 @@ const deletePost = () => {
<a v-if="activePlatform.platform_url" :href="activePlatform.platform_url"
target="_blank"
class="p-2 rounded-lg hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground">
<ExternalLink class="h-4 w-4" />
<IconExternalLink class="h-4 w-4" />
</a>
</div>
</div>
<!-- Error Message -->
<Alert v-if="activePlatform.error_message" variant="destructive">
<AlertCircle class="h-4 w-4" />
<IconAlertCircle class="h-4 w-4" />
<AlertDescription>
{{ activePlatform.error_message }}
</AlertDescription>
@ -822,14 +892,14 @@ const deletePost = () => {
:is-uploading="isUploading[activePlatform.id]" :disabled="isReadOnly"
@update:content="setContent(activePlatform.id, $event)"
@update:content-type="setContentType(activePlatform.id, $event)"
@update:meta="platformMeta[activePlatform.id] = $event"
@update:meta="setMeta(activePlatform.id, $event)"
@upload="uploadMedia($event, activePlatform.id)"
@remove-media="removeMedia(activePlatform.id, $event)"
@reorder-media="reorderMedia(activePlatform.id, $event)" />
<!-- Media Validation Errors -->
<Alert v-if="!isReadOnly && mediaValidation.length > 0" variant="destructive" class="mt-4">
<AlertCircle class="h-4 w-4" />
<IconAlertCircle class="h-4 w-4" />
<AlertDescription>
<ul class="list-disc list-inside">
<li v-for="error in mediaValidation" :key="error">{{ error }}</li>
@ -855,7 +925,7 @@ const deletePost = () => {
<!-- Empty State -->
<div v-else class="flex flex-col items-center justify-center h-full text-center p-8">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center mb-4">
<Plus class="h-8 w-8 text-muted-foreground" />
<IconPlus class="h-8 w-8 text-muted-foreground" />
</div>
<h3 class="text-lg font-semibold mb-2">No platforms selected</h3>
<p class="text-muted-foreground mb-4">Select at least one platform to create your post</p>
@ -893,7 +963,7 @@ const deletePost = () => {
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="confirmEnableSync">
<Link2 class="mr-2 h-4 w-4" />
<IconLink class="mr-2 h-4 w-4" />
Sync with {{ getPlatformLabel(firstSelectedPlatform?.platform || '') }}
</AlertDialogAction>
</AlertDialogFooter>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, useForm, router } from '@inertiajs/vue3';
import { UserPlus, Users, Mail, Trash2, Crown, User, Shield } from 'lucide-vue-next';
import { IconUserPlus, IconUsers, IconMail, IconTrash, IconCrown, IconUser, IconShield } from '@tabler/icons-vue';
import HeadingSmall from '@/components/HeadingSmall.vue';
import { Badge } from '@/components/ui/badge';
@ -93,8 +93,8 @@ function getRoleLabel(role: string): string {
}
function getRoleIcon(role: string) {
if (role === 'admin') return Shield;
return User;
if (role === 'admin') return IconShield;
return IconUser;
}
</script>
@ -114,7 +114,7 @@ function getRoleIcon(role: string) {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<UserPlus class="h-5 w-5" />
<IconUserPlus class="h-5 w-5" />
Invite Member
</CardTitle>
<CardDescription>
@ -156,7 +156,7 @@ function getRoleIcon(role: string) {
</div>
<Button type="submit" :disabled="form.processing" class="w-full">
<Mail class="mr-2 h-4 w-4" />
<IconMail class="mr-2 h-4 w-4" />
Send Invite
</Button>
</form>
@ -166,7 +166,7 @@ function getRoleIcon(role: string) {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Mail class="h-5 w-5" />
<IconMail class="h-5 w-5" />
Pending Invites
</CardTitle>
<CardDescription>
@ -194,7 +194,7 @@ function getRoleIcon(role: string) {
size="icon"
@click="cancelInvite(invite.id)"
>
<Trash2 class="h-4 w-4 text-red-500" />
<IconTrash class="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
@ -204,7 +204,7 @@ function getRoleIcon(role: string) {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Users class="h-5 w-5" />
<IconUsers class="h-5 w-5" />
Members
</CardTitle>
<CardDescription>
@ -216,7 +216,7 @@ function getRoleIcon(role: string) {
<div class="flex items-center justify-between p-3 rounded-lg border bg-muted/50">
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Crown class="h-5 w-5" />
<IconCrown class="h-5 w-5" />
</div>
<div>
<p class="font-medium">{{ owner.name }}</p>
@ -247,7 +247,7 @@ function getRoleIcon(role: string) {
size="icon"
@click="handleRemoveMember(member.id)"
>
<Trash2 class="h-4 w-4 text-red-500" />
<IconTrash class="h-4 w-4 text-red-500" />
</Button>
</div>
</div>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { Plus, Calendar, Users, Settings } from 'lucide-vue-next';
import { IconPlus, IconCalendar, IconUsers, IconSettings } from '@tabler/icons-vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@ -51,7 +51,7 @@ function switchAndNavigate(workspaceId: string, destination: string) {
</div>
<Link :href="createWorkspace.url()">
<Button>
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
Novo Workspace
</Button>
</Link>
@ -65,7 +65,7 @@ function switchAndNavigate(workspaceId: string, destination: string) {
</p>
<Link :href="createWorkspace.url()">
<Button>
<Plus class="mr-2 h-4 w-4" />
<IconPlus class="mr-2 h-4 w-4" />
Criar Workspace
</Button>
</Link>
@ -84,19 +84,19 @@ function switchAndNavigate(workspaceId: string, destination: string) {
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-muted-foreground">
<span class="flex items-center gap-1">
<Calendar class="h-4 w-4" />
<IconCalendar class="h-4 w-4" />
{{ workspace.posts_count }} posts
</span>
</div>
<div class="flex gap-2">
<Button variant="outline" size="sm" @click="switchAndNavigate(workspace.id, calendar.url())">
<Calendar class="h-4 w-4" />
<IconCalendar class="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" @click="switchAndNavigate(workspace.id, accounts.url())">
<Users class="h-4 w-4" />
<IconUsers class="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" @click="switchAndNavigate(workspace.id, settings.url())">
<Settings class="h-4 w-4" />
<IconSettings class="h-4 w-4" />
</Button>
</div>
</div>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { Calendar, Users, FileText, CheckCircle, Clock, AlertCircle, Settings } from 'lucide-vue-next';
import { IconCalendar, IconUsers, IconFileText, IconCircleCheck, IconClock, IconAlertCircle, IconSettings } from '@tabler/icons-vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -57,18 +57,18 @@ const breadcrumbs: BreadcrumbItemType[] = [
<div class="flex gap-2">
<Link :href="settings.url()">
<Button variant="ghost" size="icon">
<Settings class="h-4 w-4" />
<IconSettings class="h-4 w-4" />
</Button>
</Link>
<Link :href="accounts.url()">
<Button variant="outline">
<Users class="mr-2 h-4 w-4" />
<IconUsers class="mr-2 h-4 w-4" />
Accounts
</Button>
</Link>
<Link :href="calendar.url()">
<Button>
<Calendar class="mr-2 h-4 w-4" />
<IconCalendar class="mr-2 h-4 w-4" />
Calendar
</Button>
</Link>
@ -79,7 +79,7 @@ const breadcrumbs: BreadcrumbItemType[] = [
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Total de Posts</CardTitle>
<FileText class="h-4 w-4 text-muted-foreground" />
<IconFileText class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ stats.total_posts }}</div>
@ -89,7 +89,7 @@ const breadcrumbs: BreadcrumbItemType[] = [
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Agendados</CardTitle>
<Clock class="h-4 w-4 text-muted-foreground" />
<IconClock class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ stats.scheduled_posts }}</div>
@ -99,7 +99,7 @@ const breadcrumbs: BreadcrumbItemType[] = [
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Publicados</CardTitle>
<CheckCircle class="h-4 w-4 text-muted-foreground" />
<IconCircleCheck class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ stats.published_posts }}</div>
@ -109,7 +109,7 @@ const breadcrumbs: BreadcrumbItemType[] = [
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Contas Conectadas</CardTitle>
<Users class="h-4 w-4 text-muted-foreground" />
<IconUsers class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ stats.connected_accounts }}</div>
@ -119,14 +119,14 @@ const breadcrumbs: BreadcrumbItemType[] = [
<Card v-if="workspace.social_accounts.length === 0">
<CardContent class="flex flex-col items-center justify-center py-12">
<AlertCircle class="h-12 w-12 text-muted-foreground" />
<IconAlertCircle class="h-12 w-12 text-muted-foreground" />
<h3 class="mt-4 text-lg font-semibold">Nenhuma conta conectada</h3>
<p class="mt-2 text-sm text-muted-foreground">
Conecte suas redes sociais para começar a agendar posts.
</p>
<Link :href="accounts.url()" class="mt-4">
<Button>
<Users class="mr-2 h-4 w-4" />
<IconUsers class="mr-2 h-4 w-4" />
Conectar Contas
</Button>
</Link>

View file

@ -1,5 +1,5 @@
import { InertiaLinkProps } from '@inertiajs/vue3';
import type { LucideIcon } from 'lucide-vue-next';
import type { Component } from 'vue';
export interface Auth {
user: User;
@ -14,7 +14,7 @@ export interface BreadcrumbItem {
export interface NavItem {
title: string;
href: NonNullable<InertiaLinkProps['href']>;
icon?: LucideIcon;
icon?: Component;
isActive?: boolean;
}