This commit is contained in:
parent
576f278fc5
commit
a5bf267763
|
@ -0,0 +1,69 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
|
@ -15,29 +15,43 @@
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@codemirror/view": "^6.30.0",
|
"@codemirror/view": "^6.30.0",
|
||||||
|
"@emotion/react": "^11.13.3",
|
||||||
|
"@emotion/styled": "^11.13.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"@mui/material": "^6.0.1",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"codemirror": "^5.65.17",
|
"codemirror": "^5.65.17",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
|
"lucide-react": "^0.428.0",
|
||||||
"npm": "^10.8.2",
|
"npm": "^10.8.2",
|
||||||
"postcss": "^8.4.41",
|
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-codemirror2": "^8.0.0",
|
"react-codemirror2": "^8.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"tailwindcss": "^3.4.7",
|
"react-icons": "^5.3.0",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.4.2",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||||
"@typescript-eslint/parser": "^7.15.0",
|
"@typescript-eslint/parser": "^7.15.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.3.4"
|
"vite": "^5.3.4"
|
||||||
}
|
}
|
||||||
|
|
109
src/App.tsx
109
src/App.tsx
|
@ -1,59 +1,64 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Form from "./components/Form";
|
import FormGroup from "./components/FormGroup";
|
||||||
import Response from "./components/Response";
|
import { FaUser } from "react-icons/fa6";
|
||||||
import axios from "axios";
|
import {
|
||||||
import prettyBytes from "pretty-bytes";
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { FaUserCircle } from "react-icons/fa";
|
||||||
|
import Login from "./auth/Login";
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [response, setResponse] = useState(null);
|
const [isLoggedIn, setisLoggedIn] = useState<boolean>(false);
|
||||||
const [responseTime, setResponseTime] = useState(0);
|
const handleLogout = () => {
|
||||||
const [responseSize, setResponseSize] = useState("");
|
setisLoggedIn(false);
|
||||||
const [error, setError] = useState(null);
|
localStorage.setItem("isLoggedIn", "false");
|
||||||
|
localStorage.setItem("email", "");
|
||||||
const handleFormSubmit = async (data: any) => {
|
localStorage.setItem("ct", "");
|
||||||
try {
|
localStorage.setItem("gisData", "");
|
||||||
const startTime = new Date().getTime();
|
|
||||||
const res = await axios(data);
|
|
||||||
const endTime = new Date().getTime();
|
|
||||||
setResponseTime(endTime - startTime);
|
|
||||||
setResponse(res);
|
|
||||||
setResponseSize(
|
|
||||||
prettyBytes(
|
|
||||||
JSON.stringify(res.data).length + JSON.stringify(res.headers).length
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setError(null); // Reset error if the request is successful
|
|
||||||
} catch (error) {
|
|
||||||
setError(error);
|
|
||||||
setResponse(null); // Clear the response if there is an error
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let val = localStorage.getItem("isLoggedIn");
|
||||||
|
if (val === "true") {
|
||||||
|
setisLoggedIn(true);
|
||||||
|
} else {
|
||||||
|
setisLoggedIn(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 flex justify-center items-center">
|
<>
|
||||||
<div className="p-4 h-[90vh] w-[80vw]">
|
{isLoggedIn ? (
|
||||||
<Form onSubmit={handleFormSubmit} />
|
<div className="flex justify-center items-center flex-col">
|
||||||
{response && (
|
<nav className="w-full h-[60px] text-[#ccc343] flex-row flex justify-between ">
|
||||||
<Response
|
<div></div>
|
||||||
status={response.status}
|
<DropdownMenu>
|
||||||
time={responseTime}
|
<DropdownMenuTrigger className="text-[#db6b2a] border-none shadow-none text-[20px] hover:text-[#aaa]">
|
||||||
size={responseSize}
|
<FaUserCircle />
|
||||||
data={response.data}
|
</DropdownMenuTrigger>
|
||||||
headers={response.headers}
|
<DropdownMenuContent>
|
||||||
/>
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||||
)}
|
<DropdownMenuSeparator />
|
||||||
{error && (
|
<DropdownMenuItem
|
||||||
<Response
|
onSelect={() => {
|
||||||
status={error.response?.status}
|
handleLogout();
|
||||||
time={responseTime}
|
}}
|
||||||
size={responseSize}
|
>
|
||||||
data={error.response?.data}
|
Logout
|
||||||
headers={error.response?.headers}
|
</DropdownMenuItem>
|
||||||
/>
|
</DropdownMenuContent>
|
||||||
)}
|
</DropdownMenu>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
<FormGroup />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Login setisLoggedIn={setisLoggedIn} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,348 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import { ToastAction } from "@/components/ui/toast";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { FaLock } from "react-icons/fa6";
|
||||||
|
import { MdAlternateEmail } from "react-icons/md";
|
||||||
|
import { FaUser } from "react-icons/fa";
|
||||||
|
const Login = ({ setisLoggedIn }) => {
|
||||||
|
const [email, setemail] = useState<string>("");
|
||||||
|
const [password, setpassword] = useState<string>("");
|
||||||
|
const [confirmPassword, setconfirmPassword] = useState<string>("");
|
||||||
|
const [passMatch, setpassMatch] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
const [isLoading, setisLoading] = useState<boolean>(false);
|
||||||
|
const [tab, settab] = useState("Register");
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [error, seterror] = useState(null);
|
||||||
|
|
||||||
|
const Login = async () => {
|
||||||
|
setisLoading(true);
|
||||||
|
console.log(email, password);
|
||||||
|
const url = "http://localhost:3000/api/auth/login";
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const data = response.data;
|
||||||
|
localStorage.setItem("isLoggedIn", "true");
|
||||||
|
localStorage.setItem("email", email);
|
||||||
|
|
||||||
|
setisLoading(false);
|
||||||
|
setisLoggedIn(true);
|
||||||
|
setemail("");
|
||||||
|
setpassword("");
|
||||||
|
} catch (error) {
|
||||||
|
setisLoading(false);
|
||||||
|
|
||||||
|
if (error.response && error.response.status === 400) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Uh oh! Invalid credentials.",
|
||||||
|
description: "Email or password is incorrect.",
|
||||||
|
});
|
||||||
|
seterror("Invalid credentials.");
|
||||||
|
|
||||||
|
console.log("Uh oh! Invalid credentials.");
|
||||||
|
} else {
|
||||||
|
seterror("Something went wrong");
|
||||||
|
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Uh oh! Something went wrong.",
|
||||||
|
description: "There was a problem with your request.",
|
||||||
|
action: (
|
||||||
|
<ToastAction
|
||||||
|
altText="Try again"
|
||||||
|
onClick={() => {
|
||||||
|
Login();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</ToastAction>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const Register = async () => {
|
||||||
|
setisLoading(true);
|
||||||
|
console.log(email, password);
|
||||||
|
const url = "http://localhost:3000/api/auth/register";
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const data = response.data;
|
||||||
|
localStorage.setItem("isLoggedIn", "true");
|
||||||
|
localStorage.setItem("email", email);
|
||||||
|
|
||||||
|
setisLoading(false);
|
||||||
|
setemail("");
|
||||||
|
setpassword("");
|
||||||
|
setconfirmPassword("");
|
||||||
|
toast({
|
||||||
|
title: "Successfully Registered.",
|
||||||
|
description: "Please try logging in",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setisLoading(false);
|
||||||
|
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
setpassword("");
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Uh oh! Invalid credentials.",
|
||||||
|
description: "Email or password is incorrect.",
|
||||||
|
});
|
||||||
|
seterror("Invalid credentials.");
|
||||||
|
|
||||||
|
console.log("Uh oh! Invalid credentials.");
|
||||||
|
} else {
|
||||||
|
seterror("Something went wrong");
|
||||||
|
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Uh oh! Something went wrong.",
|
||||||
|
description: "There was a problem with your request.",
|
||||||
|
action: (
|
||||||
|
<ToastAction
|
||||||
|
altText="Try again"
|
||||||
|
onClick={() => {
|
||||||
|
Login();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</ToastAction>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setTimeout(() => {
|
||||||
|
seterror(null);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`w-full h-[100vh] bg-[#3F7CE8] flex px-[100px] justify-between items-center bg-cover bg-center `}
|
||||||
|
>
|
||||||
|
{/* <img className="w-full h-full ml-[-4rem] absolute z-0" src={"https://premiummegastructures.com/wp-content/uploads/2023/03/viber_image_2023-03-23_16-17-37-692-1024x746.jpg"} alt="" /> */}
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
{tab === "Login" ? (
|
||||||
|
<div className="w-[350px] h-[500px] rounded-[40px] bg-white flex flex-col justify-center items-center">
|
||||||
|
<h1 className="w-[80%] font-bold text-[20px] mb-8 text-[#3F7CE8]">
|
||||||
|
User Login
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className={`relative w-[80%] flex h-11 my-3 items-center justify-between rounded-[200px] border border-[#ddd] bg-background px-4 py-1 text-[12px] ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 `}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setemail(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Email Address"
|
||||||
|
className=" ring-offset-background w-[220px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 shadow-none border-none"
|
||||||
|
/>
|
||||||
|
<FaUser className="fa-solid fa-user text-[#3F7CE8] text-[17px]" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`relative w-[80%] flex h-11 my-3 items-center justify-between rounded-[200px] border border-[#ddd] bg-background px-4 py-1 text-[12px] ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 `}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setpassword(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Password"
|
||||||
|
className=" ring-offset-background w-[220px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 shadow-none border-none"
|
||||||
|
/>
|
||||||
|
<FaLock className="fa-solid fa-lock text-[#3F7CE8] text-[17px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="w-[80%] text-[red] text-start text-[13px] px-3 mb-10 border-none shadow-none">
|
||||||
|
<h6> {error ? <>{error}</> : null} </h6>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// setisModalVisible(true);
|
||||||
|
Login();
|
||||||
|
}}
|
||||||
|
className="w-[80%] text-center py-5 px-11 h-6 my-2 rounded-[20px] text-[16px] flex bg-[#3F7CE8] whitespace-nowrap text-[11px] text-white justify-center items-center hover:bg-[#1863e6]"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="m-3">
|
||||||
|
<CircularProgress color="inherit" size={25} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Login"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
settab("Register");
|
||||||
|
}}
|
||||||
|
className="w-[80%] text-[blue] text-start text-[13px] px-3 mb-1 border-none shadow-none"
|
||||||
|
>
|
||||||
|
<h6> Register an Account? </h6>
|
||||||
|
</button>
|
||||||
|
{/* <Button
|
||||||
|
onClick={() => {
|
||||||
|
toast({
|
||||||
|
title: "Scheduled: Catch up",
|
||||||
|
description: "Friday, February 10, 2023 at 5:57 PM",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show Toast
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-[350px] h-[500px] rounded-[40px] bg-white flex flex-col justify-center items-center">
|
||||||
|
<h1 className="w-[80%] font-bold text-[20px] mb-8 text-[#3F7CE8]">
|
||||||
|
Register
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className={`relative w-[80%] flex h-11 my-3 items-center justify-between rounded-[200px] border border-[#ddd] bg-background px-4 py-1 text-[12px] ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 `}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setemail(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Email Address"
|
||||||
|
className=" ring-offset-background w-[220px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 shadow-none border-none"
|
||||||
|
/>
|
||||||
|
<FaUser className="fa-solid fa-user text-[#3F7CE8] text-[17px]" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`relative w-[80%] flex h-11 my-3 items-center justify-between rounded-[200px] border border-[#ddd] bg-background px-4 py-1 text-[12px] ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 `}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setpassword(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Password"
|
||||||
|
className=" ring-offset-background w-[220px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 shadow-none border-none"
|
||||||
|
/>
|
||||||
|
<FaLock className="fa-solid fa-lock text-[#3F7CE8] text-[17px]" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`relative w-[80%] flex h-11 my-3 items-center justify-between rounded-[200px] border border-[#ddd] bg-background px-4 py-1 text-[12px] ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 `}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
setconfirmPassword(e.target.value);
|
||||||
|
setpassMatch(e.target.value === password ? true : false);
|
||||||
|
}}
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
className=" ring-offset-background w-[220px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 shadow-none border-none"
|
||||||
|
/>
|
||||||
|
<FaLock className="fa-solid fa-lock text-[#3F7CE8] text-[17px]" />
|
||||||
|
</div>
|
||||||
|
<span className="flex flex-start justify-start items-start">
|
||||||
|
{passMatch === true ? (
|
||||||
|
<h1 className=" text-[#3bd100] text-start text-[13px] px-3 mb-10 border-none shadow-none">
|
||||||
|
password matched
|
||||||
|
</h1>
|
||||||
|
) : passMatch === false ? (
|
||||||
|
<h1 className=" text-[red] text-start text-[13px] px-3 mb-10 border-none shadow-none">
|
||||||
|
password not match
|
||||||
|
</h1>
|
||||||
|
) : null}{" "}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button className="w-[80%] text-[red] text-start text-[13px] px-3 mb-1 border-none shadow-none">
|
||||||
|
<h6> {error ? <>{error}</> : null} </h6>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={email && passMatch ? false : true}
|
||||||
|
onClick={() => {
|
||||||
|
// setisModalVisible(true);
|
||||||
|
Register();
|
||||||
|
}}
|
||||||
|
className="w-[80%] text-center py-5 px-11 h-6 my-1 rounded-[20px] text-[16px] flex bg-[#3F7CE8] whitespace-nowrap text-[11px] text-white justify-center items-center hover:bg-[#1863e6]"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="m-3">
|
||||||
|
<CircularProgress color="inherit" size={25} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Register"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
settab("Login");
|
||||||
|
}}
|
||||||
|
className="w-[80%] text-[blue] text-start text-[13px] px-3 mb-1 border-none shadow-none"
|
||||||
|
>
|
||||||
|
<h6> Have an Account? </h6>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* <Button
|
||||||
|
onClick={() => {
|
||||||
|
toast({
|
||||||
|
title: "Scheduled: Catch up",
|
||||||
|
description: "Friday, February 10, 2023 at 5:57 PM",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show Toast
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
|
@ -11,9 +11,14 @@ interface JsonEditorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonEditor: React.FC<JsonEditorProps> = ({ value, onChangeVal }) => {
|
const JsonEditor: React.FC<JsonEditorProps> = ({ value, onChangeVal }) => {
|
||||||
const [editorValue, setEditorValue] = useState({});
|
const [editorValue, setEditorValue] = useState(value ?? {});
|
||||||
const [parsedValue, setParsedValue] = useState({});
|
const [parsedValue, setParsedValue] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditorValue(value ?? {})
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
|
||||||
const handleEditorChange = (newValue) => {
|
const handleEditorChange = (newValue) => {
|
||||||
// setEditorValue(newValue);
|
// setEditorValue(newValue);
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,44 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import KeyValuePair from "./KeyValuePair";
|
import KeyValuePair from "./KeyValuePair";
|
||||||
import JsonEditor from "./Editors";
|
import JsonEditor from "./Editors";
|
||||||
|
import { FaPaperPlane } from "react-icons/fa6";
|
||||||
|
import { IoMdDownload } from "react-icons/io";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
onSubmit: (data: any) => void;
|
onSubmit: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Form: React.FC<FormProps> = ({ onSubmit }) => {
|
const Form: React.FC<FormProps> = ({ onSubmit, content, setisRefresh }) => {
|
||||||
const [method, setMethod] = useState("GET");
|
const [method, setMethod] = useState(content?.method ?? "GET");
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState(content?.endpoint ?? "");
|
||||||
const [queryParams, setQueryParams] = useState([{ key: "", value: "" }]);
|
const [isSaved, setisSaved] = useState(false);
|
||||||
const [headers, setHeaders] = useState([{ key: "", value: "" }]);
|
const [queryParams, setQueryParams] = useState(
|
||||||
const [jsonBody, setJsonBody] = useState({});
|
content?.params ?? [{ key: "", value: "" }]
|
||||||
|
);
|
||||||
|
const [headers, setHeaders] = useState(
|
||||||
|
content?.header ?? [{ key: "", value: "" }]
|
||||||
|
);
|
||||||
|
const [jsonBody, setJsonBody] = useState(content ?? {});
|
||||||
const [activeTab, setActiveTab] = useState("query-params");
|
const [activeTab, setActiveTab] = useState("query-params");
|
||||||
|
|
||||||
const handleAddQueryParam = () =>
|
useEffect(() => {
|
||||||
|
setMethod(content?.method ?? "GET");
|
||||||
|
setUrl(content?.endpoint ?? "");
|
||||||
|
setQueryParams(content?.params ?? [{ key: "", value: "" }]);
|
||||||
|
setHeaders(content?.header ?? [{ key: "", value: "" }]);
|
||||||
|
setJsonBody(content?.body_json?.length > 0 ? content?.body_json[0] : {});
|
||||||
|
console.log(content?.method);
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const handleAddQueryParam = () => {
|
||||||
setQueryParams([...queryParams, { key: "", value: "" }]);
|
setQueryParams([...queryParams, { key: "", value: "" }]);
|
||||||
const handleAddHeader = () =>
|
setisSaved(false);
|
||||||
|
};
|
||||||
|
const handleAddHeader = () => {
|
||||||
setHeaders([...headers, { key: "", value: "" }]);
|
setHeaders([...headers, { key: "", value: "" }]);
|
||||||
|
setisSaved(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -42,15 +63,51 @@ const Form: React.FC<FormProps> = ({ onSubmit }) => {
|
||||||
|
|
||||||
const handleRequestEditorChange = (value: string) => {
|
const handleRequestEditorChange = (value: string) => {
|
||||||
setJsonBody(value);
|
setJsonBody(value);
|
||||||
|
setisSaved(false);
|
||||||
};
|
};
|
||||||
console.log(jsonBody);
|
console.log(jsonBody);
|
||||||
|
const updateEndpoint = async () => {
|
||||||
|
try {
|
||||||
|
// Attempt to create the endpoint
|
||||||
|
|
||||||
|
const body1 = {
|
||||||
|
endpoint: url,
|
||||||
|
method: method,
|
||||||
|
body_json: jsonBody,
|
||||||
|
// form_data: form_data,
|
||||||
|
header: headers,
|
||||||
|
params: queryParams,
|
||||||
|
};
|
||||||
|
const response1 = await axios.patch(
|
||||||
|
`http://localhost:3000/api/endpoints/update/${content?._id}`,
|
||||||
|
body1,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setisSaved(true);
|
||||||
|
// setisRefresh(true);
|
||||||
|
console.log(response1.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating or updating endpoint:", error);
|
||||||
|
setisRefresh(true);
|
||||||
|
} finally {
|
||||||
|
// Ensure the modal or creation process is closed
|
||||||
|
setisRefresh(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<select
|
<select
|
||||||
|
defaultValue={content?.method ?? method}
|
||||||
value={method}
|
value={method}
|
||||||
onChange={(e) => setMethod(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setMethod(e.target.value);
|
||||||
|
setisSaved(false);
|
||||||
|
}}
|
||||||
className="form-select border-[black] border-solid border p-1.5 outline-none mr-4 cursor-pointer"
|
className="form-select border-[black] border-solid border p-1.5 outline-none mr-4 cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="GET">GET</option>
|
<option value="GET">GET</option>
|
||||||
|
@ -61,7 +118,10 @@ const Form: React.FC<FormProps> = ({ onSubmit }) => {
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setUrl(e.target.value);
|
||||||
|
setisSaved(false);
|
||||||
|
}}
|
||||||
required
|
required
|
||||||
className="form-input flex-grow"
|
className="form-input flex-grow"
|
||||||
type="url"
|
type="url"
|
||||||
|
@ -69,10 +129,22 @@ const Form: React.FC<FormProps> = ({ onSubmit }) => {
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary border-solid border-[blue] text-[blue] rounded-lg"
|
className="btn btn-primary border-solid border-[blue] text-[blue] rounded-lg flex justify-center items-center hover:bg-[#abf9ff]"
|
||||||
>
|
>
|
||||||
Send
|
<h1 className="mr-2">Send</h1>
|
||||||
|
<FaPaperPlane />
|
||||||
</button>
|
</button>
|
||||||
|
{content?._id ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
updateEndpoint();
|
||||||
|
}}
|
||||||
|
className="flex float-right justify-center items-center hover:bg-[#ddd] "
|
||||||
|
>
|
||||||
|
<h1 className="pr-1">{isSaved ? "SAVED" : "SAVE"}</h1>
|
||||||
|
<IoMdDownload />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -133,6 +205,7 @@ const Form: React.FC<FormProps> = ({ onSubmit }) => {
|
||||||
pair={param}
|
pair={param}
|
||||||
setPairs={setQueryParams}
|
setPairs={setQueryParams}
|
||||||
pairs={queryParams}
|
pairs={queryParams}
|
||||||
|
setisSaved={setisSaved}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
|
@ -158,6 +231,7 @@ const Form: React.FC<FormProps> = ({ onSubmit }) => {
|
||||||
pair={header}
|
pair={header}
|
||||||
setPairs={setHeaders}
|
setPairs={setHeaders}
|
||||||
pairs={headers}
|
pairs={headers}
|
||||||
|
setisSaved={setisSaved}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -0,0 +1,530 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import FormSingle from "./FormSingle";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { IoClose, IoEllipsisHorizontal } from "react-icons/io5";
|
||||||
|
import { MdDeleteOutline } from "react-icons/md";
|
||||||
|
import { FaCheck, FaPlus } from "react-icons/fa6";
|
||||||
|
import { IoMdClose, IoMdDownload } from "react-icons/io";
|
||||||
|
import axios from "axios";
|
||||||
|
import ModalDelete from "./ModalDelete";
|
||||||
|
const FormGroup: React.FC = () => {
|
||||||
|
const [collections, setcollections] = useState([]);
|
||||||
|
const [activeEndpoint, setactiveEndpoint] = useState({});
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [isRefresh, setisRefresh] = useState(false);
|
||||||
|
const [isRename, setisRename] = useState(null);
|
||||||
|
const [isRenameEndpoint, setisRenameEndpoint] = useState(null);
|
||||||
|
|
||||||
|
const [newCollectionName, setnewCollectionName] = useState("");
|
||||||
|
const [newEndpointName, setnewEndpointName] = useState("");
|
||||||
|
|
||||||
|
const [isDialogOpen1, setIsDialogOpen1] = useState(false);
|
||||||
|
|
||||||
|
const [isCreateNewCollection, setisCreateNewCollection] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [collectionName, setcollectionName] = useState<string>("");
|
||||||
|
const [isCreateNewEndpoint, setisCreateNewEndpoint] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [endpointName, setendpointName] = useState<string>("");
|
||||||
|
useEffect(() => {
|
||||||
|
getCollections();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRefresh) {
|
||||||
|
getCollections();
|
||||||
|
setisRefresh(false);
|
||||||
|
}
|
||||||
|
}, [isRefresh]);
|
||||||
|
const openDialog = () => {
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
console.log("Closing dialog");
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
};
|
||||||
|
const openDialog1 = () => {
|
||||||
|
setIsDialogOpen1(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog1 = () => {
|
||||||
|
console.log("Closing dialog");
|
||||||
|
setIsDialogOpen1(false);
|
||||||
|
};
|
||||||
|
const getCollections = () => {
|
||||||
|
fetch("http://localhost:3000/api/collections/")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data);
|
||||||
|
setcollections(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.key === "Enter" || event.keyCode === 13) {
|
||||||
|
if (isCreateNewCollection === true) {
|
||||||
|
createFolder();
|
||||||
|
console.log("Enter key was pressed");
|
||||||
|
}
|
||||||
|
// Perform any action when Enter is pressed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log(isCreateNewCollection);
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isCreateNewCollection]);
|
||||||
|
const createFolder = async () => {
|
||||||
|
console.log("createFolder");
|
||||||
|
const body = {
|
||||||
|
name: collectionName,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
"http://localhost:3000/api/collections/create",
|
||||||
|
body,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(response.data);
|
||||||
|
setcollectionName("");
|
||||||
|
getCollections();
|
||||||
|
// toast({
|
||||||
|
// title: "Email Sent",
|
||||||
|
// description:
|
||||||
|
// "the file has been successfully sent to your email address",
|
||||||
|
// });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending email:", error);
|
||||||
|
}
|
||||||
|
setisCreateNewCollection(false);
|
||||||
|
};
|
||||||
|
const deleteCollection = async (collectionId) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(
|
||||||
|
"http://localhost:3000/api/collections/" + collectionId + "/delete",
|
||||||
|
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
console.log(response.data);
|
||||||
|
getCollections();
|
||||||
|
// toast({
|
||||||
|
// title: "Email Sent",
|
||||||
|
// description:
|
||||||
|
// "the file has been successfully sent to your email address",
|
||||||
|
// });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending email:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const deleteEndpoint = async (endpointId) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(
|
||||||
|
"http://localhost:3000/api/endpoints/" + endpointId + "/delete",
|
||||||
|
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setIsDialogOpen1(false);
|
||||||
|
console.log(response.data);
|
||||||
|
getCollections();
|
||||||
|
// toast({
|
||||||
|
// title: "Email Sent",
|
||||||
|
// description:
|
||||||
|
// "the file has been successfully sent to your email address",
|
||||||
|
// });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending email:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createEndpoint = async (collectionId) => {
|
||||||
|
console.log("createFolder");
|
||||||
|
const body = {
|
||||||
|
name: endpointName,
|
||||||
|
collection_id: collectionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt to create the endpoint
|
||||||
|
const response = await axios.post(
|
||||||
|
"http://localhost:3000/api/endpoints/create",
|
||||||
|
body,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the create request is successful, proceed with the update request
|
||||||
|
if (response.status === 200) {
|
||||||
|
const body1 = {
|
||||||
|
endpoint_id: response.data._id,
|
||||||
|
};
|
||||||
|
const response1 = await axios.patch(
|
||||||
|
`http://localhost:3000/api/collections/update/${collectionId}/endpoint`,
|
||||||
|
body1,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(response1.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the input field and refresh collections regardless of the outcome
|
||||||
|
setendpointName("");
|
||||||
|
getCollections();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating or updating endpoint:", error);
|
||||||
|
} finally {
|
||||||
|
// Ensure the modal or creation process is closed
|
||||||
|
setisCreateNewEndpoint(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateCollection = async (id, name) => {
|
||||||
|
try {
|
||||||
|
// Attempt to create the endpoint
|
||||||
|
|
||||||
|
const body1 = {
|
||||||
|
name: newCollectionName !== "" ? newCollectionName : name,
|
||||||
|
};
|
||||||
|
const response1 = await axios.patch(
|
||||||
|
`http://localhost:3000/api/collections/update/${id}`,
|
||||||
|
body1,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setisRename(null);
|
||||||
|
getCollections();
|
||||||
|
setnewCollectionName("");
|
||||||
|
console.log(response1.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating or updating endpoint:", error);
|
||||||
|
} finally {
|
||||||
|
// Ensure the modal or creation process is closed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateEndpoint = async (id, name) => {
|
||||||
|
try {
|
||||||
|
// Attempt to create the endpoint
|
||||||
|
|
||||||
|
const body1 = {
|
||||||
|
name: newEndpointName !== "" ? newEndpointName : name,
|
||||||
|
};
|
||||||
|
const response1 = await axios.patch(
|
||||||
|
`http://localhost:3000/api/endpoints/update/${id}`,
|
||||||
|
body1,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setisRenameEndpoint(null);
|
||||||
|
setnewEndpointName("");
|
||||||
|
getCollections();
|
||||||
|
console.log(response1.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating or updating endpoint:", error);
|
||||||
|
} finally {
|
||||||
|
// Ensure the modal or creation process is closed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-row w-full">
|
||||||
|
<div className="px-1 w-[350px] border-r-2 p-4">
|
||||||
|
<div className="w-full flex items-end justify-between p-2">
|
||||||
|
<h1 className="text-right py-4 pl-0 font-[600]">Collections</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setisCreateNewCollection(true);
|
||||||
|
}}
|
||||||
|
className=" p-3 py-2 bg-[#eee] hover:bg-[#ddd] flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<h1 className="pr-1">NEW</h1>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-1">
|
||||||
|
<Accordion type="multiple">
|
||||||
|
{collections?.map((collection) => (
|
||||||
|
<AccordionItem value={collection?._id}>
|
||||||
|
<AccordionTrigger
|
||||||
|
onDoubleClick={() => {
|
||||||
|
console.log("Double clicked");
|
||||||
|
setisRename(collection?._id);
|
||||||
|
}}
|
||||||
|
className=" py-1 shadow-none border-none"
|
||||||
|
>
|
||||||
|
{isRename === collection?._id ? (
|
||||||
|
<div className="flex flex-row justify-center items-center">
|
||||||
|
<input
|
||||||
|
defaultValue={collection.name}
|
||||||
|
onChange={(e) => setnewCollectionName(e.target.value)}
|
||||||
|
required
|
||||||
|
className="form-input flex-grow mr-3 w-[180px] "
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
updateCollection(collection?._id, collection?.name);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setisRename(null);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<IoMdClose />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onDoubleClick={() => {
|
||||||
|
console.log("Double clicked");
|
||||||
|
setisRename(collection?._id);
|
||||||
|
}}
|
||||||
|
className="text-[16px] shadow-none border-none hover:bg-[#eee] "
|
||||||
|
>
|
||||||
|
{collection?.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</AccordionTrigger>
|
||||||
|
|
||||||
|
<AccordionContent className="w-full">
|
||||||
|
<div className="w-full flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setisCreateNewEndpoint(true);
|
||||||
|
}}
|
||||||
|
className="flex float-right text-[13px] p-2 shadow-none justify-center items-center border-none hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<h1 className="pr-1">NEW</h1>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
openDialog();
|
||||||
|
}}
|
||||||
|
className="text-[16px] shadow-none border-none hover:bg-[#ffacac] rounded-full"
|
||||||
|
>
|
||||||
|
<MdDeleteOutline />
|
||||||
|
</button>
|
||||||
|
{isDialogOpen ? (
|
||||||
|
<ModalDelete
|
||||||
|
deleteCollection={deleteCollection}
|
||||||
|
closeDialog={closeDialog}
|
||||||
|
id={collection?._id}
|
||||||
|
text={`Delete this Collection '${collection.name}'?`}
|
||||||
|
model="collection"
|
||||||
|
isDialogOpen={isDialogOpen}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="list-none p-0 px-2 m-0 w-full">
|
||||||
|
{collection?.endpoints?.map((endpoint) => (
|
||||||
|
<li
|
||||||
|
className={` p-0 m-0 w-full flex justify-between flex-row`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setactiveEndpoint(endpoint);
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
console.log("Double clicked");
|
||||||
|
setisRenameEndpoint(endpoint?._id);
|
||||||
|
}}
|
||||||
|
className={`m-0 w-full shadow-none border-none text-left hover:bg-[#eee] truncate flex flex-row ${
|
||||||
|
activeEndpoint?._id === endpoint?._id
|
||||||
|
? "bg-[#f7f7f7]"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={` ${
|
||||||
|
endpoint.method === "GET"
|
||||||
|
? "text-[green]"
|
||||||
|
: endpoint.method === "POST"
|
||||||
|
? "text-[#005eca]"
|
||||||
|
: endpoint.method === "PATCH"
|
||||||
|
? "text-[#d3af10ee]"
|
||||||
|
: endpoint.method === "DELETE"
|
||||||
|
? "text-[red]"
|
||||||
|
: "text-[black]"
|
||||||
|
} font-[600] text-[12px] mr-2 w-12`}
|
||||||
|
>
|
||||||
|
{endpoint.method}
|
||||||
|
</span>{" "}
|
||||||
|
{isRenameEndpoint === endpoint?._id ? (
|
||||||
|
<div className="flex flex-row justify-center items-center">
|
||||||
|
<input
|
||||||
|
defaultValue={endpoint.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setnewEndpointName(e.target.value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
className="form-input flex-grow mr-1 w-[120px] z-5 "
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
updateEndpoint(endpoint?._id, endpoint?.name);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setisRenameEndpoint(null);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<IoMdClose />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h1
|
||||||
|
onDoubleClick={() => {
|
||||||
|
console.log("Double clicked");
|
||||||
|
setisRenameEndpoint(endpoint?._id);
|
||||||
|
}}
|
||||||
|
className="text-[12px] truncate shadow-none border-none hover:bg-[#eee] "
|
||||||
|
>
|
||||||
|
{endpoint?.name}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isRenameEndpoint !== endpoint?._id ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
openDialog1();
|
||||||
|
}}
|
||||||
|
className="text-[16px] shadow-none border-none hover:bg-[#ffabab] rounded-full"
|
||||||
|
>
|
||||||
|
<MdDeleteOutline />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isDialogOpen1 ? (
|
||||||
|
<ModalDelete
|
||||||
|
deleteEndpoint={deleteEndpoint}
|
||||||
|
closeDialog={closeDialog1}
|
||||||
|
id={endpoint?._id}
|
||||||
|
text={`Delete this Endpoint '${endpoint.name}'?`}
|
||||||
|
model="endpoint"
|
||||||
|
isDialogOpen={isDialogOpen1}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{isCreateNewEndpoint === true ? (
|
||||||
|
<div className="flex flex-row justify-center items-center">
|
||||||
|
<input
|
||||||
|
value={endpointName}
|
||||||
|
onChange={(e) => setendpointName(e.target.value)}
|
||||||
|
required
|
||||||
|
className="form-input flex-grow mr-3"
|
||||||
|
type="text"
|
||||||
|
placeholder="new endpoint name"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
createEndpoint(collection?._id);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setisCreateNewEndpoint(false);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<IoMdClose />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
{isCreateNewCollection === true ? (
|
||||||
|
<div className="flex flex-row justify-center items-center">
|
||||||
|
<input
|
||||||
|
value={collectionName}
|
||||||
|
onChange={(e) => setcollectionName(e.target.value)}
|
||||||
|
required
|
||||||
|
className="form-input flex-grow mr-3"
|
||||||
|
type="text"
|
||||||
|
placeholder="new collection name"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
createFolder();
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setisCreateNewCollection(false);
|
||||||
|
}}
|
||||||
|
className="border-none shadow-none p-1 rounded-full hover:bg-[#ddd]"
|
||||||
|
>
|
||||||
|
<IoMdClose />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className=" justify-center items-center w-full p-4 h-full">
|
||||||
|
{/* <button className="flex float-right justify-center items-center hover:bg-[#ddd] ">
|
||||||
|
<h1 className="pr-1">SAVE</h1>
|
||||||
|
<IoMdDownload />
|
||||||
|
</button> */}
|
||||||
|
{activeEndpoint ? (
|
||||||
|
<FormSingle setisRefresh={setisRefresh} content={activeEndpoint} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormGroup;
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import Response from "./Response";
|
||||||
|
import Form from "./Form";
|
||||||
|
import { IoMdDownload } from "react-icons/io";
|
||||||
|
|
||||||
|
const FormSingle: React.FC = ({ content, setisRefresh }) => {
|
||||||
|
const [response, setResponse] = useState(null);
|
||||||
|
const [responseTime, setResponseTime] = useState(0);
|
||||||
|
const [responseSize, setResponseSize] = useState("");
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [savedId, setsavedId] = useState(null);
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: any) => {
|
||||||
|
try {
|
||||||
|
const startTime = new Date().getTime();
|
||||||
|
const res = await axios(data);
|
||||||
|
const endTime = new Date().getTime();
|
||||||
|
setResponseTime(endTime - startTime);
|
||||||
|
setResponse(res);
|
||||||
|
setResponseSize(
|
||||||
|
prettyBytes(
|
||||||
|
JSON.stringify(res.data).length + JSON.stringify(res.headers).length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setError(null); // Reset error if the request is successful
|
||||||
|
} catch (error) {
|
||||||
|
setError(error);
|
||||||
|
setResponse(null); // Clear the response if there is an error
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (savedId !== content?._id) {
|
||||||
|
setsavedId(content?._id);
|
||||||
|
setResponse(null);
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className=" flex w-[100%] justify-center items-center">
|
||||||
|
<div className="p-4 h-[90vh] max-w-[75vw] w-[100%]">
|
||||||
|
<Form
|
||||||
|
setisRefresh={setisRefresh}
|
||||||
|
content={content}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
/>
|
||||||
|
{response && (
|
||||||
|
<Response
|
||||||
|
status={response.status}
|
||||||
|
time={responseTime}
|
||||||
|
size={responseSize}
|
||||||
|
data={response.data}
|
||||||
|
headers={response.headers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Response
|
||||||
|
status={error.response?.status}
|
||||||
|
time={responseTime}
|
||||||
|
size={responseSize}
|
||||||
|
data={error.response?.data}
|
||||||
|
headers={error.response?.headers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormSingle;
|
|
@ -3,26 +3,37 @@ import React from "react";
|
||||||
interface KeyValuePairProps {
|
interface KeyValuePairProps {
|
||||||
index: number;
|
index: number;
|
||||||
pair: { key: string; value: string };
|
pair: { key: string; value: string };
|
||||||
setPairs: React.Dispatch<React.SetStateAction<{ key: string; value: string }[]>>;
|
setPairs: React.Dispatch<
|
||||||
|
React.SetStateAction<{ key: string; value: string }[]>
|
||||||
|
>;
|
||||||
pairs: { key: string; value: string }[];
|
pairs: { key: string; value: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyValuePair: React.FC<KeyValuePairProps> = ({ index, pair, setPairs, pairs }) => {
|
const KeyValuePair: React.FC<KeyValuePairProps> = ({
|
||||||
|
index,
|
||||||
|
pair,
|
||||||
|
setPairs,
|
||||||
|
pairs,
|
||||||
|
setisSaved,
|
||||||
|
}) => {
|
||||||
const handleKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newPairs = [...pairs];
|
const newPairs = [...pairs];
|
||||||
newPairs[index] = { ...newPairs[index], key: e.target.value };
|
newPairs[index] = { ...newPairs[index], key: e.target.value };
|
||||||
setPairs(newPairs);
|
setPairs(newPairs);
|
||||||
|
setisSaved(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newPairs = [...pairs];
|
const newPairs = [...pairs];
|
||||||
newPairs[index] = { ...newPairs[index], value: e.target.value };
|
newPairs[index] = { ...newPairs[index], value: e.target.value };
|
||||||
setPairs(newPairs);
|
setPairs(newPairs);
|
||||||
|
setisSaved(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
const newPairs = pairs.filter((_, i) => i !== index);
|
const newPairs = pairs.filter((_, i) => i !== index);
|
||||||
setPairs(newPairs);
|
setPairs(newPairs);
|
||||||
|
setisSaved(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -41,7 +52,11 @@ const KeyValuePair: React.FC<KeyValuePairProps> = ({ index, pair, setPairs, pair
|
||||||
value={pair.value}
|
value={pair.value}
|
||||||
onChange={handleValueChange}
|
onChange={handleValueChange}
|
||||||
/>
|
/>
|
||||||
<button type="button" className="btn btn-outline-danger text-[red] border-red-400 border-solid p-1 px-3" onClick={handleRemove}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-danger text-[red] border-red-400 border-solid p-1 px-3"
|
||||||
|
onClick={handleRemove}
|
||||||
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { Controlled as CodeMirror } from "react-codemirror2";
|
||||||
|
import "codemirror/lib/codemirror.css";
|
||||||
|
import "codemirror/theme/eclipse.css"; // A white theme
|
||||||
|
import "codemirror/mode/javascript/javascript";
|
||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
interface ModalDeleteProps {
|
||||||
|
status: number;
|
||||||
|
time: number;
|
||||||
|
size: string;
|
||||||
|
data: any;
|
||||||
|
headers: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalDelete: React.FC<ModalDeleteProps> = ({
|
||||||
|
closeDialog,
|
||||||
|
isDialogOpen,
|
||||||
|
id,
|
||||||
|
text,
|
||||||
|
model,
|
||||||
|
deleteCollection,
|
||||||
|
deleteEndpoint,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="my-5">
|
||||||
|
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle></DialogTitle>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{text}</DialogTitle>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
closeDialog();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (model === "collection") {
|
||||||
|
deleteCollection(id);
|
||||||
|
} else if (model === "endpoint") {
|
||||||
|
deleteEndpoint(id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalDelete;
|
|
@ -0,0 +1,58 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
))
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
))
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
|
@ -0,0 +1,122 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
|
@ -2,10 +2,10 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{ts,tsx}",
|
'./pages/**/*.{ts,tsx}',
|
||||||
"./components/**/*.{ts,tsx}",
|
'./components/**/*.{ts,tsx}',
|
||||||
"./app/**/*.{ts,tsx}",
|
'./app/**/*.{ts,tsx}',
|
||||||
"./src/**/*.{ts,tsx}",
|
'./src/**/*.{ts,tsx}',
|
||||||
],
|
],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -71,10 +71,7 @@ module.exports = {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
},
|
},
|
||||||
fontFamily: {
|
|
||||||
sans: ["Poppins", "Arial", "sans-serif"], // Replace 'Poppins' with your desired font
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate")],
|
||||||
};
|
}
|
|
@ -7,6 +7,12 @@
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|
|
@ -7,5 +7,11 @@
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { defineConfig } from 'vite'
|
import path from "path"
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue