diff --git a/package-lock.json b/package-lock.json index fc48c35..76354bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "griller", - "version": "1.0.8", + "version": "1.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "griller", - "version": "1.0.8", + "version": "1.0.9", "license": "MIT", "dependencies": { "clsx": "^2.1.1", diff --git a/package.json b/package.json index eacb552..1fa2bcd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "griller", "license": "MIT", - "version": "1.0.9", + "version": "1.0.10", "private": false, "repository": { "url": "https://github.com/mvriu5/griller" diff --git a/src/app/lab/page.tsx b/src/app/lab/page.tsx new file mode 100644 index 0000000..bda952f --- /dev/null +++ b/src/app/lab/page.tsx @@ -0,0 +1,180 @@ +"use client"; + +import React, {useEffect, useState} from "react"; +import {ArrowLeftFromLine, ShieldAlert} from "lucide-react"; +import {useRouter} from "next/navigation"; +import {Button} from "@/lib/button"; +import {Input} from "@/lib/input"; +import {Combobox} from "@/lib/combobox"; +import {SwitchButton} from "@/lib/switchbutton"; +import {CopyButton} from "@/lib/copybutton"; +import TextareaAutosize from "react-textarea-autosize"; +import {Position, Theme, ToastProps} from "@/component/toast"; +import {useToast} from "@/component/toaster"; +import {motion} from "framer-motion"; + + +export default function Home() { + const router = useRouter(); + const { addToast } = useToast(); + + const [title, setTitle] = useState("Toast Component"); + const [secondTitle, setSecondTitle] = useState("This is a Toast Component!"); + const [position, setPosition] = useState("br"); + const [duration, setDuration] = useState(3000); + const [theme, setTheme] = useState("light"); + const [closeButton, setCloseButton] = useState(false); + const [actionButton, setActionButton] = useState(false); + const [icon, setIcon] = useState(false); + + const generateCode = () => { + return `addToast({\n title: "${title}",\n secondTitle: "${secondTitle}",\n icon: ${icon ? "" : undefined},\n position: "${position}",\n duration: ${duration},\n theme: "${theme}",\n closeButton: ${closeButton},\n actionButton: ${actionButton}\n)};`; + }; + + const [code, setCode] = useState(generateCode()); + + useEffect(() => { + setCode(generateCode()); + }, [title, secondTitle, position, duration, theme, closeButton, actionButton, icon]); + + const handleAddToast = () => { + const toast: Omit = { + title, + secondTitle, + position, + duration, + theme, + closeButton, + actionButton, + icon: icon ? : undefined + }; + addToast(toast); + }; + + return ( + +
+
+
+
router.back()} + > + +
+ + Griller/Lab + +
+ + Customize your griller component and get the code ready for production. + +
+ +
+ +
+ Customize +
+ setTitle(e.target.value)} + /> + setSecondTitle(e.target.value)} + /> +
+ setPosition(value as Position)} + /> + setDuration(parseInt(value))} + /> +
+ +
+
+ setTheme(value ? "dark" : "light")} + /> + setIcon(value)} + /> + setCloseButton(value)} + /> + setActionButton(value)} + /> +
+
+
+
+ +
+ + Generated Code + +
+
+
+ + component.tsx + + +
+ +
+
+
+ +
+
+ +
+ Made by + window.location.href = 'https://ahsmus.com'} + whileHover={{y: -4}} + whileTap={{y: -4}} + > + mvriu5 + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index a0b2d6e..221f04b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -17,9 +17,11 @@ import {Button} from "@/lib/button"; import {useToast} from "@/component/toaster"; import {motion} from "framer-motion"; import Image from "next/image" +import {useRouter} from "next/navigation"; export default function Home() { const { addToast } = useToast(); + const router = useRouter(); return (
@@ -70,6 +72,7 @@ export default function Home() {
diff --git a/src/component/toast.tsx b/src/component/toast.tsx index 4144da3..a415bbb 100644 --- a/src/component/toast.tsx +++ b/src/component/toast.tsx @@ -139,7 +139,7 @@ const Toast: React.FC {title} - {secondTitle && ( + {secondTitle && secondTitle.trim() !== "" && ( {secondTitle} @@ -175,5 +175,5 @@ const Toast: React.FC void; +} + +type ComboRef = { + getValue: () => string | number; +} + +const ComboboxPortal: React.FC<{children: ReactNode}> = ({ children }) => { + return ReactDOM.createPortal( + children, + document.body + ); +} + +const Combobox = forwardRef(({ title, values, label, preSelectedValue, onChange }, ref) => { + const [value, setValue] = useState(preSelectedValue || values[0] || ""); + const [open, setOpen] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); + const itemRef = useRef(null); + + useEffect(() => { + if (open && itemRef.current) { + const rect = itemRef.current.getBoundingClientRect(); + setDropdownPosition({ top: rect.bottom + 4, left: rect.left }); + } + }, [open]); + + const menuRef = useOutsideClick((e) => { + e.stopPropagation(); + setOpen(false); + }); + + useImperativeHandle(ref, () => ({ + getValue: () => value + }), [value]); + + const handleValueChange = (newValue: string) => { + setValue(newValue); + setOpen(false); + if (onChange) { + onChange(newValue); + } + }; + + return ( +
+ + {label && {label}} + +
setOpen(!open)} + ref={itemRef} + > + {value} + {open ? : } +
+ + {open && + +
+ {values.map((item, index) => ( +
handleValueChange(item)} + > + {item} + {value === item && } +
+ ))} +
+
+ } + +
+ ); +}); +Combobox.displayName = "Combobox"; + +export {Combobox}; +export type {ComboRef}; \ No newline at end of file diff --git a/src/lib/input.tsx b/src/lib/input.tsx new file mode 100644 index 0000000..d0bc2b7 --- /dev/null +++ b/src/lib/input.tsx @@ -0,0 +1,40 @@ +import React, {forwardRef, InputHTMLAttributes, useImperativeHandle, useState} from "react"; + +interface InputProps extends InputHTMLAttributes { + placeholder: string; + label?: string; + preSelectedValue?: string; +} + +type InputRef = { + getValue: () => string; +} + +const Input = forwardRef(({label, placeholder, preSelectedValue, ...props}, ref) => { + const [value, setValue] = useState(preSelectedValue || ""); + + useImperativeHandle(ref, () => ({ + getValue: () => value + }), [value]); + + + return ( +
+ + {label && + {label} + } + setValue(e.target.value)} + {...props} + /> +
+ ); +}); +Input.displayName = "Input"; + +export {Input}; +export type {InputRef}; \ No newline at end of file diff --git a/src/lib/switchbutton.tsx b/src/lib/switchbutton.tsx new file mode 100644 index 0000000..e31cd33 --- /dev/null +++ b/src/lib/switchbutton.tsx @@ -0,0 +1,51 @@ +import { cn } from "@/component/toast"; +import {forwardRef, useImperativeHandle, useState} from "react"; + +interface SwitchButtonProps { + label: string; + titleOne: string; + titleTwo: string; + onChange?: (value: boolean) => void; +} + +type SwitchButtonRef = { + getValue: () => boolean; +} + + +const SwitchButton = forwardRef(({ label, titleOne, titleTwo, onChange }, ref) => { + const [value, setValue] = useState(false); + + useImperativeHandle(ref, () => ({ + getValue: () => value + }), [value]); + + const handleChange = (value: boolean) => { + setValue(value); + onChange && onChange(value); + } + + return ( +
+ {label && {label}} + +
+
handleChange(!value)} + > + {titleOne} +
+
handleChange(!value)} + > + {titleTwo} +
+
+
+ + ); +}); +SwitchButton.displayName = "SwitchButton"; + +export {SwitchButton}; +export type {SwitchButtonRef}; \ No newline at end of file diff --git a/src/lib/useOutsideClick.ts b/src/lib/useOutsideClick.ts new file mode 100644 index 0000000..cd798ca --- /dev/null +++ b/src/lib/useOutsideClick.ts @@ -0,0 +1,20 @@ +import {useEffect, useRef} from "react"; + +export const useOutsideClick = (callback: (e: MouseEvent) => void) => { + const ref = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (!ref?.current?.contains(e.target as Node)) { + callback(e); + } + } + document.addEventListener("mousedown", handler); + + return () => { + document.removeEventListener("mousedown", handler); + } + }, [callback]); + + return ref; +}; \ No newline at end of file