mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 01:15:26 +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,95 @@
|
||||
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: "",
|
||||
});
|
||||
|
||||
const confirm = React.useCallback(async (title: string, options?: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
setState({
|
||||
open: true,
|
||||
resolve,
|
||||
title,
|
||||
...(options?.description !== undefined ? { description: options.description } : {}),
|
||||
...(options?.confirmText !== undefined ? { confirmText: options.confirmText } : {}),
|
||||
...(options?.cancelText !== undefined ? { 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,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,136 @@
|
||||
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: "",
|
||||
});
|
||||
|
||||
const cancelText = state.cancelText ?? "Cancel";
|
||||
const confirmText = state.confirmText ?? "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 ?? "",
|
||||
...(options?.description !== undefined ? { description: options.description } : {}),
|
||||
...(options?.defaultValue !== undefined ? { defaultValue: options.defaultValue } : {}),
|
||||
...(options?.confirmText !== undefined ? { confirmText: options.confirmText } : {}),
|
||||
...(options?.cancelText !== undefined ? { cancelText: options.cancelText } : {}),
|
||||
...(options?.inputProps !== undefined ? { 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