Next.js 16, React 19.2, Identities page, Invitations identities step, PNPM Catalogs (#381)
* Upgraded to Next.js 16 * Refactored code to comply with React 19.2 ESLint rules * Refactored some useEffect usages with the new useEffectEvent * Added Identities page and added second step to set up an identity after accepting an invitation * Updated all dependencies * Introduced PNPM catalogs for some frequently updated dependencies * Bugs fixing and improvements
This commit is contained in:
committed by
GitHub
parent
ea0c1dde80
commit
2c0d0bf7a1
@@ -14,7 +14,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"input-otp": "1.4.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"radix-ui": "1.4.3",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-top-loading-bar": "3.0.2",
|
||||
@@ -25,22 +25,22 @@
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@supabase/supabase-js": "2.58.0",
|
||||
"@tanstack/react-query": "5.90.2",
|
||||
"@supabase/supabase-js": "2.76.1",
|
||||
"@tanstack/react-query": "5.90.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/react": "19.1.16",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"next": "15.5.5",
|
||||
"eslint": "^9.38.0",
|
||||
"next": "16.0.0",
|
||||
"next-themes": "0.4.6",
|
||||
"prettier": "^3.6.2",
|
||||
"react-day-picker": "^9.11.0",
|
||||
"react-hook-form": "^7.63.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-i18next": "^16.1.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwindcss": "4.1.14",
|
||||
"tailwindcss": "4.1.15",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.3",
|
||||
"zod": "^3.25.74"
|
||||
|
||||
@@ -196,19 +196,23 @@ export const useSupabaseUpload = (options: UseSupabaseUploadOptions) => {
|
||||
|
||||
setLoading(false);
|
||||
}, [
|
||||
files,
|
||||
path,
|
||||
bucketName,
|
||||
errors,
|
||||
successes,
|
||||
onUploadSuccess,
|
||||
client,
|
||||
cacheControl,
|
||||
client.storage,
|
||||
errors,
|
||||
files,
|
||||
onUploadSuccess,
|
||||
setLoading,
|
||||
setErrors,
|
||||
setSuccesses,
|
||||
path,
|
||||
successes,
|
||||
upsert,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 0) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setErrors([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,30 +109,19 @@ export const ImageUploadInput: React.FC<Props> =
|
||||
[forwardedRef],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (image !== state.image) {
|
||||
setState((state) => ({ ...state, image }));
|
||||
}, [image]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!image) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
onRemove();
|
||||
}
|
||||
}, [image, onRemove]);
|
||||
|
||||
const Input = () => (
|
||||
<input
|
||||
{...props}
|
||||
className={cn('hidden', props.className)}
|
||||
ref={setRef}
|
||||
type={'file'}
|
||||
onInput={onInputChange}
|
||||
accept="image/*"
|
||||
aria-labelledby={'image-upload-input'}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!visible) {
|
||||
return <Input />;
|
||||
return <Input {...props} onInput={onInputChange} ref={setRef} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -140,7 +129,7 @@ export const ImageUploadInput: React.FC<Props> =
|
||||
id={'image-upload-input'}
|
||||
className={`border-input bg-background ring-primary ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring relative flex h-10 w-full cursor-pointer rounded-md border border-dashed px-3 py-2 text-sm ring-offset-2 outline-hidden transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50`}
|
||||
>
|
||||
<Input />
|
||||
<Input ref={setRef} onInput={onInputChange} />
|
||||
|
||||
<div className={'flex items-center space-x-4'}>
|
||||
<div className={'flex'}>
|
||||
@@ -198,3 +187,19 @@ export const ImageUploadInput: React.FC<Props> =
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
function Input(
|
||||
props: React.InputHTMLAttributes<unknown> & {
|
||||
ref: (input: HTMLInputElement) => void;
|
||||
},
|
||||
) {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
className={cn('hidden', props.className)}
|
||||
type={'file'}
|
||||
accept="image/*"
|
||||
aria-labelledby={'image-upload-input'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Image as ImageIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -44,25 +44,21 @@ export function ImageUploader(
|
||||
[props],
|
||||
);
|
||||
|
||||
const Input = () => (
|
||||
<ImageUploadInput
|
||||
{...control}
|
||||
accept={'image/*'}
|
||||
className={'absolute h-full w-full'}
|
||||
visible={false}
|
||||
multiple={false}
|
||||
onValueChange={onValueChange}
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value !== image) {
|
||||
setImage(props.value);
|
||||
}, [props.value]);
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
return (
|
||||
<FallbackImage descriptionSection={props.children}>
|
||||
<Input />
|
||||
<ImageUploadInput
|
||||
{...control}
|
||||
accept={'image/*'}
|
||||
className={'absolute h-full w-full'}
|
||||
visible={false}
|
||||
multiple={false}
|
||||
onValueChange={onValueChange}
|
||||
/>
|
||||
</FallbackImage>
|
||||
);
|
||||
}
|
||||
@@ -84,7 +80,14 @@ export function ImageUploader(
|
||||
alt={''}
|
||||
/>
|
||||
|
||||
<Input />
|
||||
<ImageUploadInput
|
||||
{...control}
|
||||
accept={'image/*'}
|
||||
className={'absolute h-full w-full'}
|
||||
visible={false}
|
||||
multiple={false}
|
||||
onValueChange={onValueChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -386,6 +386,7 @@ function AnimatedStep({
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setShouldRender(true);
|
||||
} else {
|
||||
const timer = setTimeout(() => setShouldRender(false), 300);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
import { AtSign, Phone } from 'lucide-react';
|
||||
import { AtSign, Mail, Phone } from 'lucide-react';
|
||||
|
||||
const DEFAULT_IMAGE_SIZE = 18;
|
||||
|
||||
@@ -33,7 +33,8 @@ export function OauthProviderLogoImage({
|
||||
|
||||
function getOAuthProviderLogos(): Record<string, string | React.ReactNode> {
|
||||
return {
|
||||
email: <AtSign className={'size-[16px]'} />,
|
||||
password: <AtSign className={'s-[18px]'} />,
|
||||
email: <Mail className={'s-[18px]'} />,
|
||||
phone: <Phone className={'size-[16px]'} />,
|
||||
google: '/images/oauth/google.webp',
|
||||
facebook: '/images/oauth/facebook.webp',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useContext, useId, useRef, useState } from 'react';
|
||||
import { useContext, useId, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
@@ -43,23 +43,20 @@ export function Sidebar(props: {
|
||||
}) => React.ReactNode);
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(props.collapsed ?? false);
|
||||
const isExpandedRef = useRef<boolean>(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const expandOnHover =
|
||||
props.expandOnHover ??
|
||||
process.env.NEXT_PUBLIC_EXPAND_SIDEBAR_ON_HOVER === 'true';
|
||||
|
||||
const sidebarSizeClassName = getSidebarSizeClassName(
|
||||
collapsed,
|
||||
isExpandedRef.current,
|
||||
);
|
||||
const sidebarSizeClassName = getSidebarSizeClassName(collapsed, isExpanded);
|
||||
|
||||
const className = getClassNameBuilder(
|
||||
cn(props.className ?? '', sidebarSizeClassName, {}),
|
||||
)();
|
||||
|
||||
const containerClassName = cn(sidebarSizeClassName, 'bg-inherit', {
|
||||
'max-w-[4rem]': expandOnHover && isExpandedRef.current,
|
||||
'max-w-[4rem]': expandOnHover && isExpanded,
|
||||
});
|
||||
|
||||
const ctx = { collapsed, setCollapsed };
|
||||
@@ -68,7 +65,7 @@ export function Sidebar(props: {
|
||||
props.collapsed && expandOnHover
|
||||
? () => {
|
||||
setCollapsed(false);
|
||||
isExpandedRef.current = true;
|
||||
setIsExpanded(true);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -77,11 +74,11 @@ export function Sidebar(props: {
|
||||
? () => {
|
||||
if (!isRadixPopupOpen()) {
|
||||
setCollapsed(true);
|
||||
isExpandedRef.current = false;
|
||||
setIsExpanded(false);
|
||||
} else {
|
||||
onRadixPopupClose(() => {
|
||||
setCollapsed(true);
|
||||
isExpandedRef.current = false;
|
||||
setIsExpanded(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,6 +121,66 @@ export function SidebarContent({
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
function SidebarGroupWrapper({
|
||||
id,
|
||||
sidebarCollapsed,
|
||||
collapsible,
|
||||
isGroupCollapsed,
|
||||
setIsGroupCollapsed,
|
||||
label,
|
||||
}: {
|
||||
id: string;
|
||||
sidebarCollapsed: boolean;
|
||||
collapsible: boolean;
|
||||
isGroupCollapsed: boolean;
|
||||
setIsGroupCollapsed: (isGroupCollapsed: boolean) => void;
|
||||
label: React.ReactNode;
|
||||
}) {
|
||||
const className = cn(
|
||||
'px-container group flex items-center justify-between space-x-2.5',
|
||||
{
|
||||
'py-2.5': !sidebarCollapsed,
|
||||
},
|
||||
);
|
||||
|
||||
if (collapsible) {
|
||||
return (
|
||||
<button
|
||||
aria-expanded={!isGroupCollapsed}
|
||||
aria-controls={id}
|
||||
onClick={() => setIsGroupCollapsed(!isGroupCollapsed)}
|
||||
className={className}
|
||||
>
|
||||
<span
|
||||
className={'text-muted-foreground text-xs font-semibold uppercase'}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
<If condition={collapsible}>
|
||||
<ChevronDown
|
||||
className={cn(`h-3 transition duration-300`, {
|
||||
'rotate-180': !isGroupCollapsed,
|
||||
})}
|
||||
/>
|
||||
</If>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (sidebarCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<span className={'text-muted-foreground text-xs font-semibold uppercase'}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarGroup({
|
||||
label,
|
||||
collapsed = false,
|
||||
@@ -138,61 +195,20 @@ export function SidebarGroup({
|
||||
const [isGroupCollapsed, setIsGroupCollapsed] = useState(collapsed);
|
||||
const id = useId();
|
||||
|
||||
const Title = (props: React.PropsWithChildren) => {
|
||||
if (sidebarCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={'text-muted-foreground text-xs font-semibold uppercase'}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = () => {
|
||||
const className = cn(
|
||||
'px-container group flex items-center justify-between space-x-2.5',
|
||||
{
|
||||
'py-2.5': !sidebarCollapsed,
|
||||
},
|
||||
);
|
||||
|
||||
if (collapsible) {
|
||||
return (
|
||||
<button
|
||||
aria-expanded={!isGroupCollapsed}
|
||||
aria-controls={id}
|
||||
onClick={() => setIsGroupCollapsed(!isGroupCollapsed)}
|
||||
className={className}
|
||||
>
|
||||
<Title>{label}</Title>
|
||||
|
||||
<If condition={collapsible}>
|
||||
<ChevronDown
|
||||
className={cn(`h-3 transition duration-300`, {
|
||||
'rotate-180': !isGroupCollapsed,
|
||||
})}
|
||||
/>
|
||||
</If>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Title>{label}</Title>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-col', {
|
||||
'gap-y-2 py-1': !collapsed,
|
||||
})}
|
||||
>
|
||||
<Wrapper />
|
||||
<SidebarGroupWrapper
|
||||
id={id}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
collapsible={collapsible}
|
||||
isGroupCollapsed={isGroupCollapsed}
|
||||
setIsGroupCollapsed={setIsGroupCollapsed}
|
||||
label={label}
|
||||
/>
|
||||
|
||||
<If condition={collapsible ? !isGroupCollapsed : true}>
|
||||
<div id={id} className={'flex flex-col space-y-1.5'}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useCallback } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
@@ -12,6 +12,54 @@ type Variant = 'numbers' | 'default' | 'dots';
|
||||
|
||||
const classNameBuilder = getClassNameBuilder();
|
||||
|
||||
function Steps({
|
||||
steps,
|
||||
currentStep,
|
||||
variant,
|
||||
}: {
|
||||
steps: string[];
|
||||
currentStep: number;
|
||||
variant?: Variant;
|
||||
}) {
|
||||
return steps.map((labelOrKey, index) => {
|
||||
const selected = currentStep === index;
|
||||
const complete = currentStep > index;
|
||||
|
||||
const className = classNameBuilder({
|
||||
selected,
|
||||
variant,
|
||||
complete,
|
||||
});
|
||||
|
||||
const isNumberVariant = variant === 'numbers';
|
||||
const isDotsVariant = variant === 'dots';
|
||||
|
||||
const labelClassName = cn({
|
||||
['px-1.5 py-2 text-xs']: !isNumberVariant,
|
||||
['hidden']: isDotsVariant,
|
||||
});
|
||||
|
||||
const { label, number } = getStepLabel(labelOrKey, index);
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div aria-selected={selected} className={className}>
|
||||
<span className={labelClassName}>
|
||||
{number}
|
||||
<If condition={!isNumberVariant}>. {label}</If>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<If condition={isNumberVariant}>
|
||||
<StepDivider selected={selected} complete={complete}>
|
||||
{label}
|
||||
</StepDivider>
|
||||
</If>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a stepper component with multiple steps.
|
||||
*
|
||||
@@ -27,46 +75,6 @@ export function Stepper(props: {
|
||||
}) {
|
||||
const variant = props.variant ?? 'default';
|
||||
|
||||
const Steps = useCallback(() => {
|
||||
return props.steps.map((labelOrKey, index) => {
|
||||
const selected = props.currentStep === index;
|
||||
const complete = props.currentStep > index;
|
||||
|
||||
const className = classNameBuilder({
|
||||
selected,
|
||||
variant,
|
||||
complete,
|
||||
});
|
||||
|
||||
const isNumberVariant = variant === 'numbers';
|
||||
const isDotsVariant = variant === 'dots';
|
||||
|
||||
const labelClassName = cn({
|
||||
['px-1.5 py-2 text-xs']: !isNumberVariant,
|
||||
['hidden']: isDotsVariant,
|
||||
});
|
||||
|
||||
const { label, number } = getStepLabel(labelOrKey, index);
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div aria-selected={selected} className={className}>
|
||||
<span className={labelClassName}>
|
||||
{number}
|
||||
<If condition={!isNumberVariant}>. {label}</If>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<If condition={isNumberVariant}>
|
||||
<StepDivider selected={selected} complete={complete}>
|
||||
{label}
|
||||
</StepDivider>
|
||||
</If>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
}, [props.steps, props.currentStep, variant]);
|
||||
|
||||
// If there are no steps, don't render anything.
|
||||
if (props.steps.length < 2) {
|
||||
return null;
|
||||
@@ -75,12 +83,16 @@ export function Stepper(props: {
|
||||
const containerClassName = cn('w-full', {
|
||||
['flex justify-between']: variant === 'numbers',
|
||||
['flex space-x-0.5']: variant === 'default',
|
||||
['flex gap-x-4 self-center']: variant === 'dots',
|
||||
['flex space-x-2.5 self-center']: variant === 'dots',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
<Steps />
|
||||
<Steps
|
||||
steps={props.steps}
|
||||
currentStep={props.currentStep}
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { RocketIcon } from 'lucide-react';
|
||||
@@ -38,12 +38,10 @@ export function VersionUpdater(props: { intervalTimeInSecond?: number }) {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [showDialog, setShowDialog] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowDialog(data?.didChange ?? false);
|
||||
}, [data?.didChange]);
|
||||
|
||||
if (!data?.didChange || dismissed) {
|
||||
return null;
|
||||
} else {
|
||||
setShowDialog(data?.didChange ?? false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -677,6 +677,7 @@ const SidebarMenuSkeleton: React.FC<
|
||||
> = ({ className, showIcon = false, ...props }) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
// eslint-disable-next-line react-hooks/purity
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user