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
57 lines
1.5 KiB
TypeScript
57 lines
1.5 KiB
TypeScript
import type { InertiaLinkProps } from '@inertiajs/vue3';
|
|
import { clsx, type ClassValue } from 'clsx';
|
|
import { twMerge } from 'tailwind-merge';
|
|
import { toast } from 'vue-sonner';
|
|
|
|
export const cn = (...inputs: ClassValue[]) => {
|
|
return twMerge(clsx(inputs));
|
|
};
|
|
|
|
export const toUrl = (href: NonNullable<InertiaLinkProps['href']>) => {
|
|
return typeof href === 'string' ? href : href?.url;
|
|
};
|
|
|
|
export const formatNumber = (value: number): string => {
|
|
return value.toLocaleString('en-US');
|
|
};
|
|
|
|
export const formatNumberCompact = (value: number): string => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
notation: 'compact',
|
|
compactDisplay: 'short',
|
|
maximumFractionDigits: 1,
|
|
}).format(value);
|
|
};
|
|
|
|
export const formatMoney = (cents: number): string => {
|
|
const dollars = cents / 100;
|
|
return dollars.toLocaleString('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
});
|
|
};
|
|
|
|
export const formatMoneyCompact = (cents: number): string => {
|
|
const dollars = cents / 100;
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
notation: 'compact',
|
|
compactDisplay: 'short',
|
|
maximumFractionDigits: 1,
|
|
}).format(dollars);
|
|
};
|
|
|
|
export const copyToClipboard = async (
|
|
text: string,
|
|
message = 'Copied to clipboard',
|
|
) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
toast.success(message);
|
|
} catch {
|
|
toast.error('Failed to copy to clipboard');
|
|
}
|
|
};
|