Skip to content

Commit

Permalink
Merge pull request #3 from mvriu5/feat/lab
Browse files Browse the repository at this point in the history
Feat/lab
  • Loading branch information
mvriu5 authored Aug 2, 2024
2 parents b75f6f2 + ac6c0da commit ef3945f
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 5 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
180 changes: 180 additions & 0 deletions src/app/lab/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Position>("br");
const [duration, setDuration] = useState(3000);
const [theme, setTheme] = useState<Theme>("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 ? "<ShieldAlert size={24} className={\"text-zinc-500\"}/>" : undefined},\n position: "${position}",\n duration: ${duration},\n theme: "${theme}",\n closeButton: ${closeButton},\n actionButton: ${actionButton}\n)};`;
};

const [code, setCode] = useState<string>(generateCode());

useEffect(() => {
setCode(generateCode());
}, [title, secondTitle, position, duration, theme, closeButton, actionButton, icon]);

const handleAddToast = () => {
const toast: Omit<ToastProps, 'id'> = {
title,
secondTitle,
position,
duration,
theme,
closeButton,
actionButton,
icon: icon ? <ShieldAlert size={24} className={"text-zinc-500"}/> : undefined
};
addToast(toast);
};

return (
<motion.div
className={"h-screen flex flex-col justify-between space-y-4 p-4 lg:px-40 lg:py-16 2xl:px-96 2xl:py-32"}
initial={{opacity: 0, filter: 'blur(10px)', y: 50}}
animate={{opacity: 1, filter: 'blur(0px)', y: 0}}
transition={{duration: 0.65}}
>
<div className={"flex flex-col space-y-4"}>
<div className={"flex flex-row items-center space-x-8 border-b border-zinc-200 pb-4"}>
<div className={"flex flex-row items-center space-x-2"}>
<div className={"rounded-lg p-1 hover:bg-zinc-200 cursor-pointer"}
onClick={() => router.back()}
>
<ArrowLeftFromLine size={20} className={"text-zinc-700"}/>
</div>
<span className={"text-lg text-zinc-700 font-medium"}>
Griller/Lab
</span>
</div>
<span className={"hidden sm:block text-zinc-500 text-sm"}>
Customize your griller component and get the code ready for production.
</span>
</div>

<div className={"grid grid-cols-1 space-y-8 lg:space-y-0 lg:grid-cols-2 lg:space-x-16 pt-4"}>

<div className={"flex flex-col space-y-2"}>
<span className={"text-sm text-zinc-700 font-medium"}>Customize</span>
<div className={"h-max flex flex-col space-y-4 p-4 rounded-lg border border-zinc-200 bg-zinc-50"}>
<Input placeholder={"Title"}
label={"Title"}
preSelectedValue={"Toast Component"}
size={40}
onChange={(e) => setTitle(e.target.value)}
/>
<Input placeholder={"Second Title"}
label={"Second Title"}
preSelectedValue={"This is a Toast Component!"}
size={60}
onChange={(e) => setSecondTitle(e.target.value)}
/>
<div className={"flex flex-col sm:flex-row sm:space-x-8 space-x-0 space-y-2 sm:space-y-0"}>
<Combobox title={"Position"}
values={["tr", "tl", "tc", "br", "bl", "bc"]}
preSelectedValue={"br"}
label={"Position"}
onChange={value => setPosition(value as Position)}
/>
<Combobox title={"Duration"}
values={["1000", "3000", "5000", "10000", "100000"]}
preSelectedValue={"3000"}
label={"Duration"}
onChange={value => setDuration(parseInt(value))}
/>
</div>

<div className={"flex flex-wrap justify-between"}>
<div className={"flex flex-wrap space-x-2"}>
<SwitchButton titleOne={"Light"}
titleTwo={"Dark"}
label={"Theme"}
onChange={value => setTheme(value ? "dark" : "light")}
/>
<SwitchButton titleOne={"No"}
titleTwo={"Yes"}
label={"Icon"}
onChange={value => setIcon(value)}
/>
<SwitchButton titleOne={"No"}
titleTwo={"Yes"}
label={"Close Button"}
onChange={value => setCloseButton(value)}
/>
<SwitchButton titleOne={"No"}
titleTwo={"Yes"}
label={"Action Button"}
onChange={value => setActionButton(value)}
/>
</div>
<Button title={"Test"}
className={"bg-zinc-50 text-zinc-700 py-1 mt-5"}
onClick={handleAddToast}
/>
</div>
</div>
</div>

<div className={"flex flex-col space-y-2"}>
<span className={"text-sm text-zinc-700 font-medium"}>
Generated Code
</span>
<div
className={"flex flex-row justify-between w-full h-max bg-zinc-50 rounded-lg border border-zinc-200"}>
<div className={"w-full h-max flex flex-col"}>
<div
className={"flex flex-row justify-between items-center px-2 py-1 bg-zinc-100 border-b border-zinc-200 rounded-t-lg"}>
<span className={"text-zinc-500 text-xs"}>
component.tsx
</span>
<CopyButton copyText={code}/>
</div>
<TextareaAutosize
className={"p-2 w-full h-auto font-mono text-sm text-zinc-500 bg-zinc-50 overflow-hidden resize-none focus:outline-none rounded-b-lg"}
value={code}
readOnly
/>
</div>
</div>
</div>

</div>
</div>

<div className={"w-full h-max flex flex-row pt-4 space-x-2 border-t border-zinc-200 text-sm text-zinc-500"}>
<span>Made by</span>
<motion.span
className={"text-zinc-700 underline cursor-pointer"}
onClick={() => window.location.href = 'https://ahsmus.com'}
whileHover={{y: -4}}
whileTap={{y: -4}}
>
mvriu5
</motion.span>
</div>
</motion.div>
);
}
3 changes: 3 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={"flex flex-col space-y-4 p-4 lg:px-40 lg:py-16 2xl:px-96 2xl:py-32"}>
Expand Down Expand Up @@ -70,6 +72,7 @@ export default function Home() {
<Button title={"Lab"}
icon={<FlaskConical size={15} className={"mr-2"}/>}
className={"py-1.5"}
onClick={() => router.push('/lab')}
/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/component/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const Toast: React.FC<ToastProps & {
>
{title}
</span>
{secondTitle && (
{secondTitle && secondTitle.trim() !== "" && (
<span className={cn("text-xs", theme === "light" ? "text-zinc-400" : "text-zinc-300", secondTitleClassname)}>
{secondTitle}
</span>
Expand Down Expand Up @@ -175,5 +175,5 @@ const Toast: React.FC<ToastProps & {
};

export {Toast};
export type {ToastProps, Position};
export type {ToastProps, Position, Theme};

97 changes: 97 additions & 0 deletions src/lib/combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, {forwardRef, ReactNode, useEffect, useImperativeHandle, useRef, useState} from "react";
import {Check, ChevronDown, ChevronUp} from "lucide-react";
import { cn } from "@/component/toast";
import {useOutsideClick} from "@/lib/useOutsideClick";
import ReactDOM from "react-dom";

interface ComboProps{
title: string;
values: string[];
label?: string;
preSelectedValue?: string;
onChange?: (value: string) => void;
}

type ComboRef = {
getValue: () => string | number;
}

const ComboboxPortal: React.FC<{children: ReactNode}> = ({ children }) => {
return ReactDOM.createPortal(
children,
document.body
);
}

const Combobox = forwardRef<ComboRef, ComboProps>(({ title, values, label, preSelectedValue, onChange }, ref) => {
const [value, setValue] = useState<string>(preSelectedValue || values[0] || "");
const [open, setOpen] = useState<boolean>(false);
const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
const itemRef = useRef<HTMLDivElement>(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 (
<div className={"flex flex-col space-y-1"}>

{label && <span className={"text-zinc-500 text-xs px-1"}>{label}</span>}

<div className={"w-max flex flex-row items-center space-x-8 justify-between px-2 py-1 bg-zinc-100 border border-zinc-200 rounded-lg text-zinc-500 text-sm cursor-pointer"}
onClick={() => setOpen(!open)}
ref={itemRef}
>
<span>{value}</span>
{open ? <ChevronDown size={16}/> : <ChevronUp size={16}/>}
</div>

{open &&
<ComboboxPortal>
<div className={"absolute z-50 w-max p-1 space-y-1 bg-zinc-100 border border-zinc-200 rounded-lg"}
style={{ top: dropdownPosition.top, left: dropdownPosition.left }}
ref={menuRef}
>
{values.map((item, index) => (
<div key={index}
className={cn(
"flex flex-row items-center space-x-2 px-1.5 py-0.5 w-full rounded-lg hover:bg-zinc-200 cursor-pointer",
value === item ? "bg-zinc-200" : ""
)}
onClick={() => handleValueChange(item)}
>
<span className={"text-zinc-500 text-sm"}>{item}</span>
{value === item && <Check size={16} className={"text-zinc-500"}/>}
</div>
))}
</div>
</ComboboxPortal>
}

</div>
);
});
Combobox.displayName = "Combobox";

export {Combobox};
export type {ComboRef};
40 changes: 40 additions & 0 deletions src/lib/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, {forwardRef, InputHTMLAttributes, useImperativeHandle, useState} from "react";

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
placeholder: string;
label?: string;
preSelectedValue?: string;
}

type InputRef = {
getValue: () => string;
}

const Input = forwardRef<InputRef, InputProps>(({label, placeholder, preSelectedValue, ...props}, ref) => {
const [value, setValue] = useState<string>(preSelectedValue || "");

useImperativeHandle(ref, () => ({
getValue: () => value
}), [value]);


return (
<div className={"flex flex-col space-y-1"}>

{label &&
<span className={"text-zinc-500 text-xs px-1"}>{label}</span>
}
<input className={"w-full px-2 py-1 bg-zinc-100 border border-zinc-200 text-zinc-500 text-sm rounded-lg focus:outline-none focus:border-zinc-300"}
placeholder={placeholder}
spellCheck={false}
value={value}
onChange={(e) => setValue(e.target.value)}
{...props}
/>
</div>
);
});
Input.displayName = "Input";

export {Input};
export type {InputRef};
Loading

0 comments on commit ef3945f

Please sign in to comment.