Skip to content
Merged
10 changes: 1 addition & 9 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import typescript from 'typescript-eslint';
/** @type {import('eslint').Linter.Config[]} */
export default [
js.configs.recommended,
reactHooks.configs.flat.recommended,
...typescript.configs.recommended,
{
...react.configs.flat.recommended,
Expand All @@ -28,15 +29,6 @@ export default [
},
},
},
{
plugins: {
'react-hooks': reactHooks,
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
{
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'],
},
Expand Down
205 changes: 132 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
"@eslint/js": "^9.19.0",
"@laravel/vite-plugin-wayfinder": "^0.1.3",
"@types/node": "^22.13.5",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-hooks": "^7.0.0",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
Expand All @@ -40,18 +41,18 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.1.11",
"@types/react": "^19.0.3",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.6.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
"globals": "^15.14.0",
"input-otp": "^1.4.2",
"laravel-vite-plugin": "^2.0",
"lucide-react": "^0.475.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.4.0",
Expand Down
7 changes: 6 additions & 1 deletion resources/js/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '../css/app.css';

import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { initializeTheme } from './hooks/use-appearance';

Expand All @@ -17,7 +18,11 @@ createInertiaApp({
setup({ el, App, props }) {
const root = createRoot(el);

root.render(<App {...props} />);
root.render(
<StrictMode>
<App {...props} />
</StrictMode>,
);
},
progress: {
color: '#4B5563',
Expand Down
28 changes: 8 additions & 20 deletions resources/js/components/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '@/components/ui/tooltip';
import { UserMenuContent } from '@/components/user-menu-content';
import { useInitials } from '@/hooks/use-initials';
import { cn } from '@/lib/utils';
import { cn, isSameUrl, resolveUrl } from '@/lib/utils';
import { dashboard } from '@/routes';
import { type BreadcrumbItem, type NavItem, type SharedData } from '@/types';
import { Link, usePage } from '@inertiajs/react';
Expand Down Expand Up @@ -118,12 +118,7 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
{rightNavItems.map((item) => (
<a
key={item.title}
href={
typeof item.href ===
'string'
? item.href
: item.href.url
}
href={resolveUrl(item.href)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 font-medium"
Expand Down Expand Up @@ -165,12 +160,10 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
href={item.href}
className={cn(
navigationMenuTriggerStyle(),
page.url ===
(typeof item.href ===
'string'
? item.href
: item.href.url) &&
activeItemStyles,
isSameUrl(
page.url,
item.href,
) && activeItemStyles,
'h-9 cursor-pointer px-3',
)}
>
Expand All @@ -182,7 +175,7 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
)}
{item.title}
</Link>
{page.url === item.href && (
{isSameUrl(page.url, item.href) && (
<div className="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"></div>
)}
</NavigationMenuItem>
Expand All @@ -209,12 +202,7 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
<Tooltip>
<TooltipTrigger>
<a
href={
typeof item.href ===
'string'
? item.href
: item.href.url
}
href={resolveUrl(item.href)}
target="_blank"
rel="noopener noreferrer"
className="group ml-1 inline-flex h-9 w-9 items-center justify-center rounded-md bg-transparent p-0 text-sm font-medium text-accent-foreground ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
Expand Down
7 changes: 2 additions & 5 deletions resources/js/components/nav-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { resolveUrl } from '@/lib/utils';
import { type NavItem } from '@/types';
import { type ComponentPropsWithoutRef } from 'react';

Expand All @@ -30,11 +31,7 @@ export function NavFooter({
className="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
>
<a
href={
typeof item.href === 'string'
? item.href
: item.href.url
}
href={resolveUrl(item.href)}
target="_blank"
rel="noopener noreferrer"
>
Expand Down
5 changes: 2 additions & 3 deletions resources/js/components/nav-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { resolveUrl } from '@/lib/utils';
import { type NavItem } from '@/types';
import { Link, usePage } from '@inertiajs/react';

Expand All @@ -19,9 +20,7 @@ export function NavMain({ items = [] }: { items: NavItem[] }) {
<SidebarMenuButton
asChild
isActive={page.url.startsWith(
typeof item.href === 'string'
? item.href
: item.href.url,
resolveUrl(item.href),
)}
tooltip={{ children: item.title }}
>
Expand Down
19 changes: 9 additions & 10 deletions resources/js/components/two-factor-setup-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ export default function TwoFactorSetupModal({
const handleModalNextStep = useCallback(() => {
if (requiresConfirmation) {
setShowVerificationStep(true);

return;
}

Expand All @@ -289,25 +288,25 @@ export default function TwoFactorSetupModal({

const resetModalState = useCallback(() => {
setShowVerificationStep(false);

if (twoFactorEnabled) {
clearSetupData();
}
}, [twoFactorEnabled, clearSetupData]);

useEffect(() => {
if (!isOpen) {
resetModalState();

return;
}

if (!qrCodeSvg) {
if (isOpen && !qrCodeSvg) {
fetchSetupData();
}
}, [isOpen, qrCodeSvg, fetchSetupData, resetModalState]);
}, [isOpen, qrCodeSvg, fetchSetupData]);

const handleClose = useCallback(() => {
resetModalState();
onClose();
}, [onClose, resetModalState]);

return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent className="sm:max-w-md">
<DialogHeader className="flex items-center justify-center">
<GridScanIcon />
Expand Down
18 changes: 9 additions & 9 deletions resources/js/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -601,10 +601,14 @@ function SidebarMenuSkeleton({
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])

// wrapping in useState to ensure the width is stable across renders
// also ensures we have a stable reference to the style object
const [skeletonStyle] = React.useState(() => (
{
"--skeleton-width": `${Math.floor(Math.random() * 40) + 50}%` // Random width between 50 to 90%.
} as React.CSSProperties
))

return (
<div
Expand All @@ -622,11 +626,7 @@ function SidebarMenuSkeleton({
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
style={skeletonStyle}
/>
</div>
)
Expand Down
2 changes: 2 additions & 0 deletions resources/js/hooks/use-appearance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export function useAppearance() {
const savedAppearance = localStorage.getItem(
'appearance',
) as Appearance | null;

// eslint-disable-next-line react-hooks/set-state-in-effect
updateAppearance(savedAppearance || 'system');

return () =>
Expand Down
29 changes: 13 additions & 16 deletions resources/js/hooks/use-mobile.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { useEffect, useState } from 'react';
import { useSyncExternalStore } from 'react';

const MOBILE_BREAKPOINT = 768;

export function useIsMobile() {
const [isMobile, setIsMobile] = useState<boolean>();

useEffect(() => {
const mql = window.matchMedia(
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
);
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);

const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
function mediaQueryListener(callback: (event: MediaQueryListEvent) => void) {
mql.addEventListener('change', callback);

mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => {
mql.removeEventListener('change', callback);
};
}

return () => mql.removeEventListener('change', onChange);
}, []);
function isSmallerThanBreakpoint() {
return mql.matches;
}

return !!isMobile;
export function useIsMobile() {
return useSyncExternalStore(mediaQueryListener, isSmallerThanBreakpoint);
}
13 changes: 6 additions & 7 deletions resources/js/layouts/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Heading from '@/components/heading';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { cn, isSameUrl, resolveUrl } from '@/lib/utils';
import { edit as editAppearance } from '@/routes/appearance';
import { edit } from '@/routes/profile';
import { show } from '@/routes/two-factor';
Expand Down Expand Up @@ -53,16 +53,15 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
<nav className="flex flex-col space-y-1 space-x-0">
{sidebarNavItems.map((item, index) => (
<Button
key={`${typeof item.href === 'string' ? item.href : item.href.url}-${index}`}
key={`${resolveUrl(item.href)}-${index}`}
size="sm"
variant="ghost"
asChild
className={cn('w-full justify-start', {
'bg-muted':
currentPath ===
(typeof item.href === 'string'
? item.href
: item.href.url),
'bg-muted': isSameUrl(
currentPath,
item.href,
),
})}
>
<Link href={item.href}>
Expand Down
12 changes: 12 additions & 0 deletions resources/js/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { InertiaLinkProps } from '@inertiajs/react';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export function isSameUrl(
url1: NonNullable<InertiaLinkProps['href']>,
url2: NonNullable<InertiaLinkProps['href']>,
) {
return resolveUrl(url1) === resolveUrl(url2);
}

export function resolveUrl(url: NonNullable<InertiaLinkProps['href']>): string {
return typeof url === 'string' ? url : url.url;
}
6 changes: 1 addition & 5 deletions resources/js/pages/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,7 @@ export default function Welcome({
strokeWidth={1}
/>
</g>
<g
/** @ts-expect-error 'plus-darker' doesn't seem to be defined in the 'csstype' module */
style={{ mixBlendMode: 'plus-darker' }}
className="translate-y-0 opacity-100 transition-all delay-300 duration-750 starting:translate-y-4 starting:opacity-0"
>
<g className="translate-y-0 opacity-100 mix-blend-plus-darker transition-all delay-300 duration-750 starting:translate-y-4 starting:opacity-0">
<path
d="M230.951 281.792L231.282 281.793C238.128 274.907 248.453 265.823 262.256 254.539C275.617 243.256 285.666 234.267 292.402 227.573C299.027 220.688 303.554 213.421 305.983 205.771C308.412 198.12 307.253 190.183 302.504 181.959C297.203 172.778 289.749 165.415 280.142 159.868C270.645 154.13 260.596 151.26 249.995 151.26C239.615 151.26 232.823 154.033 229.621 159.579C226.309 164.934 227.413 172.393 232.935 181.956L168.335 181.954C159.058 165.888 155.082 151.543 156.407 138.92C157.953 126.298 164.247 116.544 175.289 109.659C186.442 102.583 201.294 99.045 219.846 99.0457C239.281 99.0464 258.551 102.585 277.655 109.663C296.649 116.549 313.986 126.303 329.667 138.927C345.349 151.551 357.827 165.895 367.104 181.961C375.718 196.88 379.528 209.981 378.535 221.265C377.762 232.549 374.063 242.399 367.438 250.814C361.033 259.229 351.095 269.557 337.624 281.796L419.782 281.8L448.605 331.719L259.774 331.712L230.951 281.792Z"
fill="#F3BEC7"
Expand Down
6 changes: 5 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export default defineConfig({
ssr: 'resources/js/ssr.tsx',
refresh: true,
}),
react(),
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
tailwindcss(),
wayfinder({
formVariants: true,
Expand Down
Loading