mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 08:54:05 +10:00
v5.1.0 (#2970)
* chore(release): v5.1.0 * feat: implement resume thumbnails * fix: remove unused mcp tools * docs: fix formatting of docs
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@reactive-resume/ui/components/alert-dialog";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
interface ConfirmOptions {
|
||||
description?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
open: boolean;
|
||||
title: string;
|
||||
resolve: ((value: boolean) => void) | null;
|
||||
}
|
||||
|
||||
type ConfirmContextType = {
|
||||
confirm: (title: string, options?: ConfirmOptions) => Promise<boolean>;
|
||||
};
|
||||
|
||||
const ConfirmContext = React.createContext<ConfirmContextType | null>(null);
|
||||
|
||||
export function ConfirmDialogProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = React.useState<ConfirmState>({
|
||||
open: false,
|
||||
resolve: null,
|
||||
title: "",
|
||||
description: undefined,
|
||||
confirmText: undefined,
|
||||
cancelText: undefined,
|
||||
});
|
||||
|
||||
const confirm = React.useCallback(async (title: string, options?: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
setState({
|
||||
open: true,
|
||||
resolve,
|
||||
title,
|
||||
description: options?.description,
|
||||
confirmText: options?.confirmText,
|
||||
cancelText: options?.cancelText,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(true);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve]);
|
||||
|
||||
const handleCancel = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(false);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve]);
|
||||
|
||||
return (
|
||||
<ConfirmContext.Provider value={{ confirm }}>
|
||||
{children}
|
||||
|
||||
<AlertDialog open={state.open} onOpenChange={(open) => !open && handleCancel()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{state.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className={cn(!state.description && "sr-only")}>
|
||||
{state.description}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>{state.cancelText ?? "Cancel"}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>{state.confirmText ?? "Confirm"}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</ConfirmContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useConfirm() {
|
||||
const context = React.useContext(ConfirmContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useConfirm must be used within a <ConfirmDialogProvider />.");
|
||||
}
|
||||
|
||||
return context.confirm;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
interface CommonControlledStateProps<T> {
|
||||
value?: T;
|
||||
defaultValue?: T;
|
||||
}
|
||||
|
||||
type UseControlledStateProps<T, Rest extends unknown[] = []> = CommonControlledStateProps<T> & {
|
||||
onChange?: (value: T, ...args: Rest) => void;
|
||||
};
|
||||
|
||||
export function useControlledState<T, Rest extends unknown[] = []>(
|
||||
props: UseControlledStateProps<T, Rest>,
|
||||
): readonly [T, (next: T, ...args: Rest) => void] {
|
||||
const { value, defaultValue, onChange } = props;
|
||||
|
||||
const [state, setInternalState] = useState<T>(value !== undefined ? value : (defaultValue as T));
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined) setInternalState(value);
|
||||
}, [value]);
|
||||
|
||||
const setState = useCallback(
|
||||
(next: T, ...args: Rest) => {
|
||||
setInternalState(next);
|
||||
onChange?.(next, ...args);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return [state, setState] as const;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
|
||||
interface UseFormBlockerOptions {
|
||||
shouldBlock?: () => boolean;
|
||||
}
|
||||
|
||||
type BlockableFormStore = {
|
||||
get: () => {
|
||||
isDirty: boolean;
|
||||
isSubmitting: boolean;
|
||||
};
|
||||
subscribe: {
|
||||
(observer: {
|
||||
next?: (value: { isDirty: boolean; isSubmitting: boolean }) => void;
|
||||
error?: (error: unknown) => void;
|
||||
complete?: () => void;
|
||||
}): { unsubscribe: () => void };
|
||||
(
|
||||
next: (value: { isDirty: boolean; isSubmitting: boolean }) => void,
|
||||
error?: (error: unknown) => void,
|
||||
complete?: () => void,
|
||||
): { unsubscribe: () => void };
|
||||
};
|
||||
};
|
||||
|
||||
export function useFormBlocker<TStore extends BlockableFormStore>(
|
||||
form: { store: TStore },
|
||||
options?: UseFormBlockerOptions,
|
||||
) {
|
||||
const confirm = useConfirm();
|
||||
const closeDialog = useDialogStore((state) => state.closeDialog);
|
||||
const setOnBeforeClose = useDialogStore((state) => state.setOnBeforeClose);
|
||||
|
||||
const isDirty = useStore(form.store, (state) => state.isDirty);
|
||||
const isSubmitting = useStore(form.store, (state) => state.isSubmitting);
|
||||
|
||||
const shouldBlock = useCallback(() => {
|
||||
if (options?.shouldBlock) return options.shouldBlock();
|
||||
return isDirty && !isSubmitting;
|
||||
}, [options, isDirty, isSubmitting]);
|
||||
|
||||
const confirmClose = useCallback(async () => {
|
||||
if (!shouldBlock()) return true;
|
||||
|
||||
return confirm(t`Are you sure you want to close this dialog?`, {
|
||||
description: t`You have unsaved changes that will be lost.`,
|
||||
confirmText: t`Leave`,
|
||||
cancelText: t`Stay`,
|
||||
});
|
||||
}, [shouldBlock, confirm]);
|
||||
|
||||
const requestClose = useCallback(async () => {
|
||||
const confirmed = await confirmClose();
|
||||
if (!confirmed) return;
|
||||
|
||||
closeDialog();
|
||||
}, [confirmClose, closeDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
setOnBeforeClose(confirmClose);
|
||||
return () => setOnBeforeClose(null);
|
||||
}, [confirmClose, setOnBeforeClose]);
|
||||
|
||||
return { requestClose };
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(MOBILE_QUERY);
|
||||
const onChange = (e: MediaQueryListEvent) => {
|
||||
setIsMobile(e.matches);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(mql.matches);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import * as React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@reactive-resume/ui/components/alert-dialog";
|
||||
import { Input } from "@reactive-resume/ui/components/input";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
type PromptOptions = {
|
||||
description?: string;
|
||||
defaultValue?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
inputProps?: Omit<React.ComponentProps<typeof Input>, "value" | "onChange" | "onKeyDown">;
|
||||
};
|
||||
|
||||
type PromptState = PromptOptions & {
|
||||
open: boolean;
|
||||
title: string;
|
||||
value: string;
|
||||
resolve: ((value: string | null) => void) | null;
|
||||
};
|
||||
|
||||
type PromptContextType = {
|
||||
prompt: (title: string, options?: PromptOptions) => Promise<string | null>;
|
||||
};
|
||||
|
||||
const PromptContext = React.createContext<PromptContextType | null>(null);
|
||||
|
||||
export function PromptDialogProvider({ children }: { children: React.ReactNode }) {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [state, setState] = React.useState<PromptState>({
|
||||
open: false,
|
||||
resolve: null,
|
||||
title: "",
|
||||
value: "",
|
||||
description: undefined,
|
||||
defaultValue: undefined,
|
||||
confirmText: undefined,
|
||||
cancelText: undefined,
|
||||
inputProps: undefined,
|
||||
});
|
||||
|
||||
const cancelText = state.cancelText ?? t`Cancel`;
|
||||
const confirmText = state.confirmText ?? t`Confirm`;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!state.open) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.focus();
|
||||
}, 0);
|
||||
}, [state.open]);
|
||||
|
||||
const prompt = React.useCallback(async (title: string, options?: PromptOptions): Promise<string | null> => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
setState({
|
||||
open: true,
|
||||
resolve,
|
||||
title,
|
||||
value: options?.defaultValue ?? "",
|
||||
description: options?.description,
|
||||
defaultValue: options?.defaultValue,
|
||||
confirmText: options?.confirmText,
|
||||
cancelText: options?.cancelText,
|
||||
inputProps: options?.inputProps,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(state.value);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve, state.value]);
|
||||
|
||||
const handleCancel = React.useCallback(() => {
|
||||
if (state.resolve) state.resolve(null);
|
||||
|
||||
setState((prev) => ({ ...prev, open: false, resolve: null }));
|
||||
}, [state.resolve]);
|
||||
|
||||
const handleValueChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState((prev) => ({ ...prev, value: e.target.value }));
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") handleConfirm();
|
||||
},
|
||||
[handleConfirm],
|
||||
);
|
||||
|
||||
return (
|
||||
<PromptContext.Provider value={{ prompt }}>
|
||||
{children}
|
||||
|
||||
<AlertDialog open={state.open} onOpenChange={(open) => !open && handleCancel()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{state.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className={cn(!state.description && "sr-only")}>
|
||||
{state.description}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={state.value}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleValueChange}
|
||||
{...state.inputProps}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>{cancelText}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>{confirmText}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</PromptContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePrompt() {
|
||||
const context = React.useContext(PromptContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("usePrompt must be used within a <PromptDialogProvider />.");
|
||||
}
|
||||
|
||||
return context.prompt;
|
||||
}
|
||||
Reference in New Issue
Block a user