feat: add reusable prompt dialogs for frontend forms
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography } from "@mui/material";
|
||||
|
||||
type PromptOptions = {
|
||||
title?: string;
|
||||
message: string;
|
||||
defaultValue?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
};
|
||||
|
||||
type PromptContextValue = {
|
||||
prompt: (options: PromptOptions) => Promise<string | null>;
|
||||
};
|
||||
|
||||
type PromptState = PromptOptions & {
|
||||
open: boolean;
|
||||
resolver?: (value: string | null) => void;
|
||||
};
|
||||
|
||||
const PromptContext = createContext<PromptContextValue | null>(null);
|
||||
|
||||
export function PromptProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState<PromptState>({
|
||||
open: false,
|
||||
title: "Enter value",
|
||||
message: "",
|
||||
defaultValue: "",
|
||||
confirmLabel: "Save",
|
||||
cancelLabel: "Cancel",
|
||||
});
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const closeWith = useCallback((next: string | null) => {
|
||||
setState((prev) => {
|
||||
prev.resolver?.(next);
|
||||
return { ...prev, open: false, resolver: undefined };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const prompt = useCallback((options: PromptOptions) => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
setValue(options.defaultValue ?? "");
|
||||
setState({
|
||||
open: true,
|
||||
title: options.title ?? "Enter value",
|
||||
message: options.message,
|
||||
defaultValue: options.defaultValue ?? "",
|
||||
confirmLabel: options.confirmLabel ?? "Save",
|
||||
cancelLabel: options.cancelLabel ?? "Cancel",
|
||||
resolver: resolve,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(() => ({ prompt }), [prompt]);
|
||||
|
||||
return (
|
||||
<PromptContext.Provider value={contextValue}>
|
||||
{children}
|
||||
<Dialog open={state.open} onClose={() => closeWith(null)} fullWidth maxWidth="xs">
|
||||
<DialogTitle>{state.title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography sx={{ color: "text.secondary", mb: 1.5 }}>{state.message}</Typography>
|
||||
<TextField autoFocus fullWidth value={value} onChange={(e) => setValue(e.target.value)} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => closeWith(null)}>{state.cancelLabel}</Button>
|
||||
<Button variant="contained" onClick={() => closeWith(value)}>{state.confirmLabel}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</PromptContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePrompt() {
|
||||
const ctx = useContext(PromptContext);
|
||||
if (!ctx) throw new Error("usePrompt must be used within PromptProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user