feat: adding tests

This commit is contained in:
Paulo Castellano 2026-01-18 22:28:27 -03:00
parent fb732495b2
commit 9ffb3bb98e
45 changed files with 385 additions and 120 deletions

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/** @type {import('tailwindcss').Config} */
module.exports = {
presets: [

View file

@ -1,12 +1,13 @@
import '../css/app.css';
import { createInertiaApp } from '@inertiajs/vue3';
import { configureEcho } from '@laravel/echo-vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { i18nVue } from 'laravel-vue-i18n';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import { i18nVue } from 'laravel-vue-i18n';
import { initializeTheme } from './composables/useAppearance';
import { configureEcho } from '@laravel/echo-vue';
configureEcho({
broadcaster: 'reverb',

View file

@ -5,7 +5,6 @@ import { Calendar, Menu, Settings, Share2, Users } from 'lucide-vue-next';
import { computed } from 'vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import WorkspaceSwitcher from '@/components/WorkspaceSwitcher.vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
@ -27,9 +26,10 @@ import {
SheetTrigger,
} from '@/components/ui/sheet';
import UserMenuContent from '@/components/UserMenuContent.vue';
import WorkspaceSwitcher from '@/components/WorkspaceSwitcher.vue';
import { useActiveUrl } from '@/composables/useActiveUrl';
import { calendar } from '@/routes';
import { getInitials } from '@/composables/useInitials';
import { calendar } from '@/routes';
import type { BreadcrumbItem, NavItem } from '@/types';
interface Props {

View file

@ -1,4 +1,7 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { ref } from 'vue';
import {
AlertDialog,
AlertDialogAction,
@ -9,8 +12,6 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { router } from '@inertiajs/vue3';
import { ref } from 'vue';
const props = defineProps({
title: {

View file

@ -1,4 +1,8 @@
<script setup lang="ts">
import { parseDate } from '@internationalized/date';
import { IconCalendar } from '@tabler/icons-vue';
import { ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { InputMask } from '@/components/ui/input';
@ -9,9 +13,6 @@ import {
} from '@/components/ui/popover';
import { useDateMaska } from '@/composables/useDateMaska';
import dayjs from '@/dayjs';
import { parseDate } from '@internationalized/date';
import { IconCalendar } from '@tabler/icons-vue';
import { ref, watch } from 'vue';
const props = defineProps({
name: {

View file

@ -1,4 +1,8 @@
<script setup lang="ts">
import { IconCheck, IconChevronDown, IconSearch } from '@tabler/icons-vue';
import { FocusScope } from 'reka-ui';
import { ref, watchEffect } from 'vue';
import { Button } from '@/components/ui/button';
import {
Combobox,
@ -11,9 +15,6 @@ import {
ComboboxList,
ComboboxTrigger,
} from '@/components/ui/combobox';
import { IconCheck, IconChevronDown, IconSearch } from '@tabler/icons-vue';
import { FocusScope } from 'reka-ui';
import { ref, watchEffect } from 'vue';
interface Language {
id: string;

View file

@ -1,4 +1,9 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import axios from 'axios';
import { Trash2, Upload, User } from 'lucide-vue-next';
import { ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -13,10 +18,6 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { store as storeMedia, destroy as destroyMedia } from '@/routes/medias';
import { router } from '@inertiajs/vue3';
import axios from 'axios';
import { Trash2, Upload, User } from 'lucide-vue-next';
import { ref, watch } from 'vue';
interface Photo {
url: string;

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { computed, onMounted, onUnmounted } from 'vue';
import { AlertCircle, Check, ExternalLink, RefreshCw, Trash2 } from 'lucide-vue-next';
import { computed, onMounted, onUnmounted } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';

View file

@ -1,4 +1,8 @@
<script setup lang="ts">
import { IconCheck, IconChevronDown, IconSearch } from '@tabler/icons-vue';
import { FocusScope } from 'reka-ui';
import { ref, watchEffect } from 'vue';
import { Button } from '@/components/ui/button';
import {
Combobox,
@ -11,9 +15,6 @@ import {
ComboboxList,
ComboboxTrigger,
} from '@/components/ui/combobox';
import { IconCheck, IconChevronDown, IconSearch } from '@tabler/icons-vue';
import { FocusScope } from 'reka-ui';
import { ref, watchEffect } from 'vue';
interface Timezone {
value: string;

View file

@ -1,7 +1,5 @@
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import {
IconAlertTriangle,
IconCircleCheck,
@ -9,6 +7,8 @@ import {
IconInfoCircle,
IconX,
} from '@tabler/icons-vue';
import { computed, ref, watch } from 'vue';
const show = ref(false);
const animate = ref(false);

View file

@ -35,7 +35,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import {
IconDots,
IconVideo,
IconX,
IconPhoto,
IconThumbUp,
@ -36,7 +35,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
IconDots,
IconVideo,
@ -11,6 +10,8 @@ import {
IconBookmark,
IconMusic,
} from '@tabler/icons-vue';
import { computed } from 'vue';
import {
Select,
SelectContent,
@ -58,7 +59,6 @@ const props = defineProps<Props>();
// Computed helpers for content type
const isReel = computed(() => props.contentType === 'instagram_reel');
const isStory = computed(() => props.contentType === 'instagram_story');
const isFeed = computed(() => props.contentType === 'instagram_feed' || !props.contentType);
const hasMultipleContentTypes = computed(() => (props.contentTypeOptions?.length || 0) > 1);
const emit = defineEmits<{

View file

@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
IconWorld,
IconThumbUp,
@ -7,7 +6,6 @@ import {
IconRepeat,
IconSend,
IconDots,
IconVideo,
IconX,
IconPhoto,
IconHeart,
@ -39,7 +37,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -36,7 +36,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
IconX,
IconPhoto,
IconShare,
IconUpload,
} from '@tabler/icons-vue';
import { computed } from 'vue';
import {
Select,
SelectContent,
@ -173,7 +174,7 @@ const updateBoardId = (boardId: string) => {
<div v-else class="relative">
<div class="flex overflow-x-auto snap-x snap-mandatory scrollbar-hide">
<div
v-for="(item, index) in media"
v-for="item in media"
:key="item.id"
class="relative flex-shrink-0 w-full snap-center"
>

View file

@ -1,15 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue';
import LinkedInPreview from './LinkedInPreview.vue';
import XPreview from './XPreview.vue';
import BlueskyPreview from './BlueskyPreview.vue';
import FacebookPreview from './FacebookPreview.vue';
import InstagramPreview from './InstagramPreview.vue';
import LinkedInPreview from './LinkedInPreview.vue';
import MastodonPreview from './MastodonPreview.vue';
import PinterestPreview from './PinterestPreview.vue';
import ThreadsPreview from './ThreadsPreview.vue';
import TikTokPreview from './TikTokPreview.vue';
import XPreview from './XPreview.vue';
import YouTubePreview from './YouTubePreview.vue';
import PinterestPreview from './PinterestPreview.vue';
import BlueskyPreview from './BlueskyPreview.vue';
import MastodonPreview from './MastodonPreview.vue';
interface SocialAccount {
id: string;

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import {
IconDots,
IconVideo,
IconX,
IconPhoto,
IconHeart,
@ -36,7 +35,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -37,7 +37,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import {
IconDots,
IconVideo,
IconX,
IconPhoto,
IconHeart,
@ -38,7 +37,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -36,7 +36,7 @@ interface Props {
isUploading?: boolean;
}
const props = defineProps<Props>();
defineProps<Props>();
const emit = defineEmits<{
'update:content': [value: string];

View file

@ -59,7 +59,7 @@ export default {
formatDuration(duration: string) {
// Duration comes in format "HH:mm:ss"
const [hours, minutes, seconds] = duration.split(':');
const [hours, minutes] = duration.split(':');
const h = parseInt(hours, 10);
const m = parseInt(minutes, 10);

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { home } from '@/routes';
defineProps<{

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { home } from '@/routes';
defineProps<{

View file

@ -1,19 +1,19 @@
<script setup lang="ts">
import { ref } from 'vue';
import { IconInfoCircle } from '@tabler/icons-vue';
import { ref } from 'vue';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { store as storeBluesky } from '@/routes/social/bluesky';
interface Props {
errors?: Record<string, string>;
}
const props = defineProps<Props>();
defineProps<Props>();
const formRef = ref<HTMLFormElement | null>(null);
const identifier = ref('');

View file

@ -1,10 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Facebook, Check } from 'lucide-vue-next';
import { ref } from 'vue';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { select as selectFacebookPage } from '@/routes/social/facebook';
interface Page {

View file

@ -2,9 +2,9 @@
import { Head } from '@inertiajs/vue3';
import { ref } from 'vue';
import AppLayout from '@/layouts/AppLayout.vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
import SocialAccountsGrid, { type Platform } from '@/components/SocialAccountsGrid.vue';
import AppLayout from '@/layouts/AppLayout.vue';
import { accounts } from '@/routes';
import { disconnect as disconnectAccount } from '@/routes/accounts';
import { type BreadcrumbItemType } from '@/types';
@ -19,7 +19,7 @@ interface Props {
platforms: Platform[];
}
const props = defineProps<Props>();
defineProps<Props>();
const deleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);

View file

@ -1,10 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Building2, Check } from 'lucide-vue-next';
import { ref } from 'vue';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { select as selectLinkedInPage } from '@/routes/social/linkedin-page';
interface Organization {

View file

@ -1,19 +1,19 @@
<script setup lang="ts">
import { ref } from 'vue';
import { IconInfoCircle } from '@tabler/icons-vue';
import { ref } from 'vue';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import PopupLayout from '@/layouts/PopupLayout.vue';
import { authorize as authorizeMastodon } from '@/routes/social/mastodon';
interface Props {
errors?: Record<string, string>;
}
const props = defineProps<Props>();
defineProps<Props>();
const formRef = ref<HTMLFormElement | null>(null);
const instance = ref('https://mastodon.social');

View file

@ -2,11 +2,11 @@
import { Head } from '@inertiajs/vue3';
import { CreditCard, FileText, Building2, ExternalLink, Sparkles } from 'lucide-vue-next';
import AppLayout from '@/layouts/AppLayout.vue';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import AppLayout from '@/layouts/AppLayout.vue';
import { type BreadcrumbItemType } from '@/types';
interface Subscription {
@ -40,7 +40,7 @@ interface Props {
defaultPaymentMethod: PaymentMethod | null;
}
const props = defineProps<Props>();
defineProps<Props>();
const breadcrumbs: BreadcrumbItemType[] = [
{

View file

@ -1,14 +1,14 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Plus, Hash, Pencil, Trash2 } from 'lucide-vue-next';
import { ref } from 'vue';
import AppLayout from '@/layouts/AppLayout.vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
import CreateDialog from '@/components/hashtags/CreateDialog.vue';
import EditDialog from '@/components/hashtags/EditDialog.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { index as hashtagsIndex, destroy as hashtagsDestroy } from '@/routes/hashtags';
import { type BreadcrumbItemType } from '@/types';

View file

@ -1,11 +1,11 @@
<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
import { login, register } from '@/routes';
import { ref } from 'vue';
const props = defineProps<{
invite: {

View file

@ -1,14 +1,14 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Plus, Tag, Pencil, Trash2 } from 'lucide-vue-next';
import { ref } from 'vue';
import AppLayout from '@/layouts/AppLayout.vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
import CreateDialog from '@/components/labels/CreateDialog.vue';
import EditDialog from '@/components/labels/EditDialog.vue';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { index as labelsIndex, destroy as labelsDestroy } from '@/routes/labels';
import { type BreadcrumbItemType } from '@/types';

View file

@ -1,11 +1,10 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { Building, Building2, Rocket, Sparkles, Store, User } from 'lucide-vue-next';
import { computed } from 'vue';
import OnboardingLayout from '@/layouts/OnboardingLayout.vue';
import { Button } from '@/components/ui/button';
import { storeStep1 } from '@/actions/App/Http/Controllers/OnboardingController';
import { Button } from '@/components/ui/button';
import OnboardingLayout from '@/layouts/OnboardingLayout.vue';
interface Persona {
value: string;
@ -18,7 +17,7 @@ interface Props {
personas: Persona[];
}
const props = defineProps<Props>();
defineProps<Props>();
const form = useForm({
persona: '',

View file

@ -2,10 +2,10 @@
import { Head, router } from '@inertiajs/vue3';
import { ref, computed } from 'vue';
import OnboardingLayout from '@/layouts/OnboardingLayout.vue';
import { storeStep2 } from '@/actions/App/Http/Controllers/OnboardingController';
import SocialAccountsGrid, { type Platform } from '@/components/SocialAccountsGrid.vue';
import { Button } from '@/components/ui/button';
import { storeStep2 } from '@/actions/App/Http/Controllers/OnboardingController';
import OnboardingLayout from '@/layouts/OnboardingLayout.vue';
interface Props {
platforms: Platform[];

View file

@ -1,12 +1,12 @@
<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { computed } from 'vue';
import { ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next';
import dayjs from '@/dayjs';
import { computed } from 'vue';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
import { calendar } from '@/routes';
import { create as createPost, edit as editPost, show as showPost } from '@/routes/posts';
import { type BreadcrumbItemType } from '@/types';

View file

@ -1,16 +1,13 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import { Clock, AlertCircle, Trash2, Send, Link2, MoreHorizontal, Plus } from 'lucide-vue-next';
import axios from 'axios';
import dayjs from '@/dayjs';
import { PlatformPreview } from '@/components/posts/previews';
import { Clock, AlertCircle, Trash2, Send, Link2, MoreHorizontal, Plus } from 'lucide-vue-next';
import { computed, ref, watch } from 'vue';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { store as storeMedia, storeChunked as storeMediaChunked, destroy as destroyMedia, duplicate as duplicateMedia } from '@/actions/App/Http/Controllers/MediaController';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
import DatePicker from '@/components/DatePicker.vue';
import { PlatformPreview } from '@/components/posts/previews';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
AlertDialog,
@ -22,6 +19,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@ -29,22 +27,17 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import DatePicker from '@/components/DatePicker.vue';
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
import { calendar } from '@/routes';
import { destroy as destroyPost, update as updatePost } from '@/routes/posts';
import { store as storeMedia, storeChunked as storeMediaChunked, destroy as destroyMedia, duplicate as duplicateMedia } from '@/actions/App/Http/Controllers/MediaController';
import { uploadChunked, shouldUseChunkedUpload } from '@/utils/chunkedUpload';
import { type BreadcrumbItemType } from '@/types';
import { uploadChunked, shouldUseChunkedUpload } from '@/utils/chunkedUpload';
interface SocialAccount {
id: string;
@ -276,10 +269,6 @@ function getContentTypeOptions(platform: string): ContentTypeOption[] {
return contentTypeOptions[platform] || [];
}
function hasMultipleContentTypes(platform: string): boolean {
return (contentTypeOptions[platform]?.length || 0) > 1;
}
function getPlatformData(platform: string): Record<string, any> {
if (platform === 'pinterest') {
return { boards: props.pinterestBoards };
@ -344,11 +333,6 @@ const selectedPlatforms = computed(() => {
return props.post.post_platforms.filter(pp => selectedPlatformIds.value.includes(pp.id));
});
// Unselected platforms (for add button)
const unselectedPlatforms = computed(() => {
return props.post.post_platforms.filter(pp => !selectedPlatformIds.value.includes(pp.id));
});
// Other selected platforms (for sync label)
const otherSelectedPlatforms = computed(() => {
return selectedPlatforms.value.filter(pp => pp.id !== activeTabId.value);

View file

@ -0,0 +1,274 @@
<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { Clock, CheckCircle, AlertCircle, Loader2, FileText, Plus, Eye, Pencil, Trash2 } from 'lucide-vue-next';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
import { calendar } from '@/routes';
import { index as postsIndex, create as createPost, show as showPost, edit as editPost, destroy as destroyPost } from '@/routes/posts';
import { type BreadcrumbItemType } from '@/types';
interface SocialAccount {
id: string;
platform: string;
display_name: string;
username: string;
avatar_url: string | null;
}
interface PostPlatform {
id: string;
social_account_id: string;
enabled: boolean;
platform: string;
content: string;
status: string;
social_account: SocialAccount;
}
interface User {
id: string;
name: string;
}
interface Post {
id: string;
status: string;
scheduled_at: string | null;
published_at: string | null;
post_platforms: PostPlatform[];
user: User;
}
interface PaginatedPosts {
data: Post[];
links: {
first: string | null;
last: string | null;
prev: string | null;
next: string | null;
};
meta: {
current_page: number;
from: number | null;
last_page: number;
per_page: number;
to: number | null;
total: number;
};
}
interface Workspace {
id: string;
name: string;
timezone: string;
}
interface Props {
workspace: Workspace;
posts: PaginatedPosts;
}
const props = defineProps<Props>();
const breadcrumbs: BreadcrumbItemType[] = [
{ title: 'Posts', href: postsIndex.url() },
];
const getPlatformLogo = (platform: string): string => {
const logos: Record<string, string> = {
'linkedin': '/images/accounts/linkedin.png',
'linkedin-page': '/images/accounts/linkedin.png',
'x': '/images/accounts/x.png',
'tiktok': '/images/accounts/tiktok.png',
'youtube': '/images/accounts/youtube.png',
'facebook': '/images/accounts/facebook.png',
'instagram': '/images/accounts/instagram.png',
'threads': '/images/accounts/threads.png',
'pinterest': '/images/accounts/pinterest.png',
'bluesky': '/images/accounts/bluesky.png',
'mastodon': '/images/accounts/mastodon.png',
};
return logos[platform] || '/images/accounts/default.png';
};
const getStatusConfig = (status: string) => {
const configs: Record<string, { label: string; color: string; icon: typeof FileText }> = {
'draft': { label: 'Rascunho', color: 'bg-gray-100 text-gray-800', icon: FileText },
'scheduled': { label: 'Agendado', color: 'bg-blue-100 text-blue-800', icon: Clock },
'publishing': { label: 'Publicando', color: 'bg-yellow-100 text-yellow-800', icon: Loader2 },
'published': { label: 'Publicado', color: 'bg-green-100 text-green-800', icon: CheckCircle },
'partially_published': { label: 'Parcialmente Publicado', color: 'bg-orange-100 text-orange-800', icon: AlertCircle },
'failed': { label: 'Falhou', color: 'bg-red-100 text-red-800', icon: AlertCircle },
};
return configs[status] || configs['draft'];
};
const formatDateTime = (date: string | null): string => {
if (!date) return '-';
return dayjs.utc(date).tz(props.workspace.timezone).format('D MMM YYYY, HH:mm');
};
const getEnabledPlatforms = (post: Post) => {
return post.post_platforms.filter(pp => pp.enabled);
};
const getPostPreview = (post: Post): string => {
const enabledPlatforms = getEnabledPlatforms(post);
if (enabledPlatforms.length === 0) return 'Sem conteudo';
const firstPlatform = enabledPlatforms[0];
const content = firstPlatform.content || '';
return content.length > 100 ? content.substring(0, 100) + '...' : content || 'Sem conteudo';
};
const canEdit = (post: Post): boolean => {
return ['draft', 'scheduled'].includes(post.status);
};
const handleDelete = (post: Post) => {
if (confirm('Tem certeza que deseja excluir este post?')) {
router.delete(destroyPost.url(post.id));
}
};
</script>
<template>
<Head title="Posts" />
<AppLayout :breadcrumbs="breadcrumbs">
<div class="flex flex-col gap-6 p-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Posts</h1>
<p class="text-muted-foreground">
Gerencie todos os seus posts
</p>
</div>
<div class="flex gap-2">
<Link :href="calendar.url()">
<Button variant="outline">
<Clock class="mr-2 h-4 w-4" />
Calendario
</Button>
</Link>
<Link :href="createPost.url()">
<Button>
<Plus class="mr-2 h-4 w-4" />
Novo Post
</Button>
</Link>
</div>
</div>
<div v-if="posts.data.length === 0">
<Card>
<CardContent class="flex flex-col items-center justify-center py-12">
<FileText class="h-12 w-12 text-muted-foreground" />
<h3 class="mt-4 text-lg font-semibold">Nenhum post ainda</h3>
<p class="mt-2 text-sm text-muted-foreground">
Comece criando seu primeiro post.
</p>
<Link :href="createPost.url()" class="mt-4">
<Button>
<Plus class="mr-2 h-4 w-4" />
Criar Post
</Button>
</Link>
</CardContent>
</Card>
</div>
<div v-else class="space-y-4">
<Card v-for="post in posts.data" :key="post.id">
<CardContent class="p-4">
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<Badge :class="getStatusConfig(post.status).color">
<component :is="getStatusConfig(post.status).icon" class="mr-1 h-3 w-3" />
{{ getStatusConfig(post.status).label }}
</Badge>
<span class="text-sm text-muted-foreground">
{{ formatDateTime(post.scheduled_at) }}
</span>
</div>
<p class="text-sm text-foreground line-clamp-2 mb-3">
{{ getPostPreview(post) }}
</p>
<div class="flex items-center gap-2">
<div class="flex -space-x-2">
<img
v-for="pp in getEnabledPlatforms(post).slice(0, 5)"
:key="pp.id"
:src="getPlatformLogo(pp.platform)"
:alt="pp.platform"
class="h-6 w-6 rounded-full ring-2 ring-background"
/>
</div>
<span v-if="getEnabledPlatforms(post).length > 5" class="text-xs text-muted-foreground">
+{{ getEnabledPlatforms(post).length - 5 }}
</span>
<span class="text-xs text-muted-foreground ml-2">
por {{ post.user.name }}
</span>
</div>
</div>
<div class="flex items-center gap-1">
<Link :href="showPost.url(post.id)">
<Button variant="ghost" size="icon">
<Eye class="h-4 w-4" />
</Button>
</Link>
<Link v-if="canEdit(post)" :href="editPost.url(post.id)">
<Button variant="ghost" size="icon">
<Pencil class="h-4 w-4" />
</Button>
</Link>
<Button
v-if="canEdit(post)"
variant="ghost"
size="icon"
@click="handleDelete(post)"
>
<Trash2 class="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</CardContent>
</Card>
<div v-if="posts.meta.last_page > 1" class="flex items-center justify-between">
<p class="text-sm text-muted-foreground">
Mostrando {{ posts.meta.from }} a {{ posts.meta.to }} de {{ posts.meta.total }} posts
</p>
<div class="flex gap-2">
<Link
v-if="posts.links.prev"
:href="posts.links.prev"
preserve-scroll
>
<Button variant="outline" size="sm">
Anterior
</Button>
</Link>
<Link
v-if="posts.links.next"
:href="posts.links.next"
preserve-scroll
>
<Button variant="outline" size="sm">
Proximo
</Button>
</Link>
</div>
</div>
</div>
</div>
</AppLayout>
</template>

View file

@ -3,12 +3,12 @@ import { Head, Link } from '@inertiajs/vue3';
import { useEcho } from '@laravel/echo-vue';
import { Clock, CheckCircle, AlertCircle, Loader2, FileText, ExternalLink, Pencil } from 'lucide-vue-next';
import { ref, computed } from 'vue';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
import { calendar } from '@/routes';
import { edit as editPost } from '@/routes/posts';
import { type BreadcrumbItemType } from '@/types';

View file

@ -11,7 +11,7 @@ import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import AppLayout from '@/layouts/AppLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import { members } from '@/routes';
import { members as membersRoute } from '@/routes';
import { destroy as destroyInvite, store as storeInvite } from '@/routes/invites';
import { remove as removeMember } from '@/routes/members';
import { type BreadcrumbItem } from '@/types';
@ -49,10 +49,10 @@ interface Props {
roles: Role[];
}
const props = defineProps<Props>();
defineProps<Props>();
const breadcrumbItems: BreadcrumbItem[] = [
{ title: 'Members', href: members.url() },
{ title: 'Members', href: membersRoute.url() },
];
const form = useForm({

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
import { ref } from 'vue';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import DeleteUser from '@/components/DeleteUser.vue';
@ -15,7 +16,6 @@ import SettingsLayout from '@/layouts/settings/Layout.vue';
import { edit } from '@/routes/profile';
import { send } from '@/routes/verification';
import { type BreadcrumbItem } from '@/types';
import { ref } from 'vue';
interface Language {
id: string;

View file

@ -1,11 +1,11 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { type BreadcrumbItemType } from '@/types';
const breadcrumbs: BreadcrumbItemType[] = [

View file

@ -2,9 +2,9 @@
import { Head, Link, router } from '@inertiajs/vue3';
import { Plus, Calendar, Users, Settings } from 'lucide-vue-next';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { accounts, calendar } from '@/routes';
import { settings } from '@/routes/workspace';
import { create as createWorkspace, index as workspacesIndex, switchMethod } from '@/routes/workspaces';

View file

@ -2,9 +2,9 @@
import { Head, Link } from '@inertiajs/vue3';
import { Calendar, Users, FileText, CheckCircle, Clock, AlertCircle, Settings } from 'lucide-vue-next';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { accounts, calendar } from '@/routes';
import { settings } from '@/routes/workspace';
import { type BreadcrumbItemType } from '@/types';
@ -35,7 +35,7 @@ interface Props {
stats: Stats;
}
const props = defineProps<Props>();
defineProps<Props>();
const breadcrumbs: BreadcrumbItemType[] = [
{ title: 'Calendar', href: calendar.url() },

View file

@ -20,5 +20,7 @@ abstract class TestCase extends BaseTestCase
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
}