trypost/resources/js/components/UserMenuContent.vue
Paulo Castellano f3f72afecd refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale
Auth pages:
- Create AuthSplitLayout with animated feature slides (6 slides, 3 languages)
- All auth pages use split layout (form left, visual right)
- Add show/hide password toggle with tooltip on Register
- Legal footer only shown on Register via showLegal prop

Subscribe page:
- Redesign to match auth card pattern (centered, clean)
- Platform icons, feature checklist, dynamic trial days (trialDays - 1)
- Add "Switch workspace" link
- Full i18n (en, es, pt-BR)

Onboarding:
- Rename URLs: step1 -> role, step2 -> connect
- Add enforceStep() to prevent skipping/going back steps
- Redirect /onboarding to /onboarding/role
- Redesign Step2 with AuthSplitLayout and compact platform list
- 21 tests covering all step enforcement scenarios

Workspaces page:
- Redesign with AuthSplitLayout (list with avatars, current badge)

Language system:
- Move locale from DB to cookie (forever, unencrypted, session.domain)
- Create SetLocale middleware (sets cookie if missing, validates against config)
- Rename lang/pt-br to lang/pt-BR
- Add dayjs es locale

Other:
- Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard)
- ConfirmDeleteModal with text confirmation (sendkit pattern)
- i18n for ConfirmDeleteModal internal strings (common.php)
- EmptyState component for posts index
- Exact match for "All" posts in sidebar
- Posts breadcrumbs show current status filter
- DialogFooter buttons aligned left
- API Keys page redesign with Table, DropdownMenu, EmptyState
- Extract CreateApiKeyDialog and InviteMemberDialog to components
- Remove API Keys from sidebar
- DropdownMenuItem destructive variant for Remove action
2026-03-30 11:53:42 -03:00

159 lines
5.2 KiB
Vue

<script setup lang="ts">
import { Link, router, usePage } from '@inertiajs/vue3';
import {
IconBrightnessUp,
IconDeviceDesktop,
IconLanguage,
IconLogout,
IconMoon,
IconUser,
} from '@tabler/icons-vue';
import { loadLanguageAsync } from 'laravel-vue-i18n';
import { computed } from 'vue';
import {
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from '@/components/ui/dropdown-menu';
import UserInfo from '@/components/UserInfo.vue';
import { useAppearance } from '@/composables/useAppearance';
import dayjs from '@/dayjs';
import { logout } from '@/routes';
import { edit } from '@/routes/app/profile';
import { updateLanguage } from '@/actions/App/Http/Controllers/App/Settings/ProfileController';
import type { User } from '@/types';
interface Language {
code: string;
name: string;
}
type Props = {
user: User;
};
defineProps<Props>();
const page = usePage();
const languages = computed<Language[]>(() => page.props.languages as Language[]);
const currentLanguage = computed(() => languages.value?.find((l: Language) => l.code === page.props.locale));
const { appearance, updateAppearance } = useAppearance();
const switchLanguage = (code: string) => {
const previousCode = currentLanguage.value?.code || 'en';
loadLanguageAsync(code);
dayjs.locale(code.toLowerCase());
router.patch(updateLanguage.url(), { locale: code }, {
preserveScroll: true,
preserveState: false,
onError: () => {
loadLanguageAsync(previousCode);
dayjs.locale(previousCode.toLowerCase());
},
});
};
const handleLogout = () => {
router.flushAll();
};
</script>
<template>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserInfo
:user="user"
:show-email="true"
fallback-class="bg-neutral-200 text-neutral-600 dark:bg-neutral-700 dark:text-neutral-200"
/>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem :as-child="true">
<Link class="block w-full cursor-pointer" :href="edit()" prefetch>
<IconUser class="size-4" />
Account
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger class="gap-2">
<IconBrightnessUp
v-if="appearance === 'light'"
class="size-4 text-muted-foreground"
/>
<IconMoon
v-else-if="appearance === 'dark'"
class="size-4 text-muted-foreground"
/>
<IconDeviceDesktop
v-else
class="size-4 text-muted-foreground"
/>
Theme: <span class="capitalize">{{ appearance }}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem @click="updateAppearance('light')">
<IconBrightnessUp class="size-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem @click="updateAppearance('dark')">
<IconMoon class="size-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="updateAppearance('system')">
<IconDeviceDesktop class="size-4" />
System
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuSub v-if="languages && languages.length > 1">
<DropdownMenuSubTrigger class="gap-2">
<IconLanguage class="size-4 text-muted-foreground" />
Language: <span>{{ currentLanguage?.name ?? 'English' }}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
v-for="language in languages"
:key="language.code"
:class="language.code === currentLanguage?.code ? 'bg-accent' : ''"
@click="switchLanguage(language.code)"
>
{{ language.name }}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem :as-child="true">
<Link
class="block w-full cursor-pointer"
:href="logout()"
@click="handleLogout"
as="button"
data-test="logout-button"
>
<IconLogout class="size-4" />
Log out
</Link>
</DropdownMenuItem>
</template>