fix: resolve all TypeScript errors

- Fix Auth type: add currentWorkspace, workspaces, FlashData
- Fix User.id type: number → string (UUID)
- Fix trans() number params: wrap with String()
- Fix debounce NodeJS.Timeout → ReturnType<typeof setTimeout>
- Fix DatePicker name prop: required → optional
- Fix DatePicker emit typing
- Fix storeStep1 → storeRole import in onboarding/Role
- Fix duplicate id in AcceptInvite interface
- Fix formatDate → formatDateTime in ApiKeys
- Add @unovis/vue and @unovis/ts type declarations
- Install @unovis/vue @unovis/ts dev dependencies
- Add type casts for Inertia/Reka UI component props
This commit is contained in:
Paulo Castellano 2026-03-31 09:34:39 -03:00
parent c020419db1
commit 07ddedceae
20 changed files with 1996 additions and 34 deletions

1931
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,8 @@
"@laravel/vite-plugin-wayfinder": "^0.1.3",
"@tailwindcss/vite": "^4.1.11",
"@types/node": "^22.13.5",
"@unovis/ts": "^1.6.4",
"@unovis/vue": "^1.6.4",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-typescript": "^14.3.0",
"chokidar": "^5.0.0",

View file

@ -10,6 +10,7 @@ import { createApp, h } from 'vue';
import { initializeTheme } from './composables/useAppearance';
import dayjs from './dayjs';
import posthog from './posthog';
import type { Auth } from './types';
configureEcho({
broadcaster: 'reverb',
@ -31,7 +32,7 @@ createInertiaApp({
// Set dayjs locale based on user's language
dayjs.locale(locale.toLowerCase());
const auth = props.initialPage.props.auth as { user?: { id: string; email: string; name: string }; currentWorkspace?: { id: string; name: string } } | undefined;
const auth = props.initialPage.props.auth as Auth | undefined;
if (auth?.user) {
posthog.identify(auth.user.id, {

View file

@ -83,9 +83,9 @@ const remove = () => {
const method = props.method as 'delete' | 'get' | 'post' | 'put' | 'patch';
if (method === 'delete' || method === 'get') {
router[method](url.value, options);
router[method](url.value, options as any);
} else {
router[method](url.value, {}, options);
router[method](url.value, {}, options as any);
}
};

View file

@ -22,7 +22,7 @@ import dayjs from '@/dayjs';
const props = defineProps({
name: {
type: String,
required: true,
default: '',
},
modelValue: {
type: String,
@ -47,7 +47,9 @@ const props = defineProps({
},
});
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits<{
'update:modelValue': [value: string | null];
}>();
// Parse input value into date
const parseInput = (value: string) => {
@ -180,7 +182,7 @@ const displayText = computed(() => {
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" :align="align">
<Calendar v-model="internalDate" :placeholder="internalDate" layout="month-and-year" locale="en"
<Calendar v-model="internalDate as any" :placeholder="(internalDate as any)" layout="month-and-year" locale="en"
calendar-label="Date picker" initial-focus />
<!-- Time Picker -->
<div v-if="showTime" class="border-t p-3">

View file

@ -78,7 +78,7 @@ watchEffect(() => {
<Combobox
:model-value="selectedTimezone"
@update:model-value="
(v: Timezone) => {
(v: any) => {
selectedTimezone = v;
emit('update:modelValue', v?.value || null);
}

View file

@ -4,8 +4,9 @@ import { computed, onMounted, watch } from 'vue';
import { toast } from 'vue-sonner';
import { Toaster } from '@/components/ui/sonner';
const style = computed(() => usePage().props.flash?.bannerStyle || 'success');
const message = computed(() => usePage().props.flash?.banner || '');
const page = usePage();
const style = computed(() => (page.props.flash as Record<string, string>)?.bannerStyle || 'success');
const message = computed(() => (page.props.flash as Record<string, string>)?.banner || '');
const showToast = (msg: string) => {
switch (style.value) {

View file

@ -403,7 +403,7 @@ const handleDropOnItem = (e: DragEvent, targetId: string) => {
{{ validationMessage }}
</span>
</div>
<Textarea :model-value="content" @update:model-value="emit('update:content', $event)"
<Textarea :model-value="content" @update:model-value="emit('update:content', $event as string)"
:placeholder="$t('posts.form.write_caption')" class="min-h-[120px] resize-none" :disabled="props.disabled" />
</div>
</div>

View file

@ -2,7 +2,7 @@ export default function debounce(
callback: (...args: any[]) => void,
wait = 1000,
) {
let timeoutId: NodeJS.Timeout | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const debouncedFn = (...args: any[]) => {
if (timeoutId) {

View file

@ -10,7 +10,6 @@ import { type SharedData } from '@/types';
const props = defineProps<{
invite: {
id: string;
id: string;
email: string;
role: {

View file

@ -82,7 +82,7 @@ const getStatusVariant = (status: string): 'default' | 'secondary' | 'destructiv
<IconSparkles class="h-4 w-4 text-primary" />
<AlertTitle>{{ $t('billing.trial.title') }}</AlertTitle>
<AlertDescription>
{{ $t('billing.trial.description', { date: trialEndsAt }) }}
{{ $t('billing.trial.description', { date: trialEndsAt ?? '' }) }}
</AlertDescription>
</Alert>

View file

@ -72,7 +72,7 @@ const featureKeys = [
{{ $t('billing.subscribe.title') }}
</h1>
<p class="mt-2 text-sm text-muted-foreground">
{{ trans('billing.subscribe.description', { days: displayDays }) }}
{{ trans('billing.subscribe.description', { days: String(displayDays) }) }}
</p>
</div>
@ -111,7 +111,7 @@ const featureKeys = [
:disabled="processing"
@click="subscribe"
>
{{ trans('billing.subscribe.start_trial', { days: displayDays }) }}
{{ trans('billing.subscribe.start_trial', { days: String(displayDays) }) }}
</Button>
</div>

View file

@ -134,7 +134,7 @@ const getHashtagCount = (hashtags: string): number => {
</div>
</div>
<CardDescription>
{{ $t('hashtags.hashtags_count', { count: getHashtagCount(hashtag.hashtags) }) }}
{{ $t('hashtags.hashtags_count', { count: String(getHashtagCount(hashtag.hashtags)) }) }}
</CardDescription>
</CardHeader>
<CardContent>

View file

@ -3,7 +3,7 @@ import { Head, useForm } from '@inertiajs/vue3';
import { IconBuilding, IconRocket, IconSparkles, IconBuildingStore, IconUser } from '@tabler/icons-vue';
import { trans } from 'laravel-vue-i18n';
import { storeStep1 } from '@/actions/App/Http/Controllers/App/OnboardingController';
import { storeRole } from '@/actions/App/Http/Controllers/App/OnboardingController';
import { Button } from '@/components/ui/button';
import OnboardingLayout from '@/layouts/OnboardingLayout.vue';
@ -34,7 +34,7 @@ const icons: Record<string, typeof IconRocket> = {
};
const submit = () => {
form.post(storeStep1.url());
form.post(storeRole.url());
};
const isSelected = (value: string) => form.persona === value;

View file

@ -203,7 +203,7 @@ const goToDate = (dateStr: string) => {
});
};
const switchView = (view: string) => {
const switchView = (view: string | number) => {
router.get(calendar.url({ query: { view } }), {}, {
preserveState: true,
});
@ -270,7 +270,7 @@ const formatTime = (scheduledAt: string): string => {
<Button variant="outline" size="icon" @click="navigate(1)">
<IconChevronRight class="h-4 w-4" />
</Button>
<DatePicker v-if="isMobile" v-model="selectedDate" @update:model-value="goToDate" />
<DatePicker v-if="isMobile" v-model="selectedDate" @update:model-value="(v: any) => goToDate(v)" />
</template>
<template #header-center>
@ -460,7 +460,7 @@ const formatTime = (scheduledAt: string): string => {
</Link>
<div v-if="getPostsForDay(day).length > 3"
class="text-xs text-muted-foreground px-2 py-0.5">
{{ $t('calendar.more', { count: getPostsForDay(day).length - 3 }) }}
{{ $t('calendar.more', { count: String(getPostsForDay(day).length - 3) }) }}
</div>
</div>
</div>

View file

@ -563,13 +563,13 @@ const contentValidation = computed(() => {
} else if (hasUnsupportedVideos) {
results[pp.id] = { valid: false, message: trans('posts.edit.validation.videos_not_supported'), charCount, maxLength: config.maxContentLength };
} else if (hasTooManyImages) {
results[pp.id] = { valid: false, message: trans('posts.edit.validation.max_images', { count: config.maxImages }), charCount, maxLength: config.maxContentLength };
results[pp.id] = { valid: false, message: trans('posts.edit.validation.max_images', { count: String(config.maxImages) }), charCount, maxLength: config.maxContentLength };
} else if (!config.supportsTextOnly && !hasMedia) {
results[pp.id] = { valid: false, message: trans('posts.edit.validation.requires_media'), charCount, maxLength: config.maxContentLength };
} else if (!hasContent && !hasMedia) {
results[pp.id] = { valid: false, message: trans('posts.edit.no_content'), charCount, maxLength: config.maxContentLength };
} else if (!withinLimit) {
results[pp.id] = { valid: false, message: trans('posts.edit.validation.exceeded', { count: charCount - config.maxContentLength }), charCount, maxLength: config.maxContentLength };
results[pp.id] = { valid: false, message: trans('posts.edit.validation.exceeded', { count: String(charCount - config.maxContentLength) }), charCount, maxLength: config.maxContentLength };
} else {
results[pp.id] = { valid: true, message: `${charCount}/${config.maxContentLength}`, charCount, maxLength: config.maxContentLength };
}
@ -594,7 +594,7 @@ const mediaValidation = computed(() => {
}
if (imageCount > config.maxImages && config.maxImages > 0) {
errors.push(trans('posts.edit.validation.supports_up_to_images', { platform: getPlatformLabel(pp.platform), count: config.maxImages }));
errors.push(trans('posts.edit.validation.supports_up_to_images', { platform: getPlatformLabel(pp.platform), count: String(config.maxImages) }));
}
if (!config.allowedMediaTypes.includes('video') && videoCount > 0) {

View file

@ -125,7 +125,7 @@ const confirmDeleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(n
</Badge>
</TableCell>
<TableCell class="text-muted-foreground">
{{ token.expires_at ? date.formatDate(token.expires_at) : $t('settings.api_keys.table.never') }}
{{ token.expires_at ? date.formatDateTime(token.expires_at) : $t('settings.api_keys.table.never') }}
</TableCell>
<TableCell class="text-muted-foreground">
{{ token.last_used_at ? date.diffForHumans(token.last_used_at) : $t('settings.api_keys.table.never') }}

View file

@ -54,7 +54,7 @@ const switchToWorkspace = (workspace: Workspace) => {
<div class="min-w-0 flex-1">
<p class="truncate font-medium">{{ workspace.name }}</p>
<p class="text-xs text-muted-foreground">
{{ trans('workspaces.connections', { count: workspace.social_accounts_count }) }} · {{ trans('workspaces.posts', { count: workspace.posts_count }) }}
{{ trans('workspaces.connections', { count: String(workspace.social_accounts_count) }) }} · {{ trans('workspaces.posts', { count: String(workspace.posts_count) }) }}
</p>
</div>
<Badge v-if="workspace.id === currentWorkspaceId" variant="secondary" class="shrink-0">

View file

@ -1,5 +1,26 @@
import { AppPageProps } from '@/types/index';
declare module '@unovis/vue' {
export const VisXYContainer: any;
export const VisLine: any;
export const VisAxis: any;
export const VisArea: any;
export const VisStackedBar: any;
export const VisBulletLegend: any;
export const VisTooltip: any;
export const VisCrosshair: any;
}
declare module '@unovis/ts' {
export class Line<T = any> {}
export class Area<T = any> {}
export class StackedBar<T = any> {}
export class Axis<T = any> {}
export class Crosshair<T = any> {}
export class Tooltip<T = any> {}
export class BulletLegend<T = any> {}
}
declare global {
interface Window {
dataLayer: Record<string, unknown>[];

View file

@ -1,9 +1,27 @@
import { InertiaLinkProps } from '@inertiajs/vue3';
import type { Component } from 'vue';
export interface Workspace {
id: string;
name: string;
logo_url: string | null;
timezone: string;
role?: 'owner' | 'admin' | 'member' | null;
[key: string]: unknown;
}
export interface Auth {
user: User;
role: 'owner' | 'admin' | 'member' | null;
currentWorkspace: Workspace | null;
workspaces: Workspace[];
}
export interface FlashData {
banner?: string;
bannerStyle?: 'success' | 'danger' | 'info' | 'warning';
plainToken?: string;
[key: string]: unknown;
}
export interface BreadcrumbItem {
@ -24,6 +42,7 @@ export interface NavItem {
export interface SharedData {
name: string;
auth: Auth;
flash: FlashData;
sidebarOpen: boolean;
selfHosted: boolean;
[key: string]: unknown;
@ -34,7 +53,7 @@ export type AppPageProps<
> = T & SharedData;
export interface User {
id: number;
id: string;
name: string;
email: string;
has_photo: boolean;