new design added
This commit is contained in:
parent
704fb6cee1
commit
2c0ab8b736
8 changed files with 271 additions and 41 deletions
37
src/Home.jsx
37
src/Home.jsx
|
|
@ -11,35 +11,52 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { generateToken } from './api/authApi';
|
import { generateToken } from './api/authApi';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
import { getProjectById } from './api/projectApi';
|
||||||
|
import { Loader } from 'lucide-react';
|
||||||
|
import CanvasContext from './components/Context/canvasContext/CanvasContext';
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
const { canvas, selectedPanel } = useContext(CanvasContext);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const getToken = localStorage.getItem('canvas_token');
|
|
||||||
|
|
||||||
const { id } = params;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const getToken = () => localStorage.getItem('canvas_token');
|
||||||
|
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
// Fetch token only if it doesn't exist
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['get-token'],
|
queryKey: ['get-token'],
|
||||||
queryFn: () => generateToken(id),
|
queryFn: () => generateToken(id),
|
||||||
enabled: !getToken
|
});
|
||||||
})
|
|
||||||
console.log(data);
|
// Fetch project data only if token and id exist
|
||||||
|
const { data: projectData, isLoading: projectLoading } = useQuery({
|
||||||
|
queryKey: ['project', id],
|
||||||
|
queryFn: async () => await getProjectById(id),
|
||||||
|
enabled: !!getToken() && !!id && selectedPanel === "project",
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!getToken && !isLoading) {
|
const token = getToken(); // Get latest token
|
||||||
|
|
||||||
|
if (!token && !isLoading) {
|
||||||
navigate("/unAuthenticated");
|
navigate("/unAuthenticated");
|
||||||
}
|
}
|
||||||
if (getToken && !isLoading && data?.status === 201) {
|
if (token && !isLoading && data?.status === 201) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
}, [getToken, navigate, isLoading, data])
|
if (projectData?.status === 200 & !projectLoading && projectData?.data?.object && canvas && canvas?._objects?.length === 0 && selectedPanel === "project") {
|
||||||
|
canvas.loadFromJSON(projectData?.data?.object);
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
}, [navigate, isLoading, data, projectData, projectLoading, canvas, selectedPanel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden relative">
|
<div className="flex h-screen overflow-hidden relative">
|
||||||
{
|
{
|
||||||
isLoading && <div>Loading...</div>
|
isLoading && <p><Loader className="animate-spin mx-auto" /></p>
|
||||||
}
|
}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ interface Project {
|
||||||
|
|
||||||
export const getProjects = async () => {
|
export const getProjects = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects`, {
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects/`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,12 @@ const CanvasSetting = () => {
|
||||||
const createEmptyProject = async () => {
|
const createEmptyProject = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await createProject();
|
const response = await createProject();
|
||||||
console.log(response);
|
if (response?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: response?.status,
|
||||||
|
description: response?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
if (response?.data?.id) {
|
if (response?.data?.id) {
|
||||||
navigate(`/${response.data.id}`);
|
navigate(`/${response.data.id}`);
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +47,7 @@ const CanvasSetting = () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
createEmptyProject();
|
createEmptyProject();
|
||||||
}
|
}
|
||||||
}, [id, navigate]);
|
}, [id, navigate, toast]);
|
||||||
|
|
||||||
// upload bg-image handler
|
// upload bg-image handler
|
||||||
const { mutate: uploadBackgroundImage } = useMutation({
|
const { mutate: uploadBackgroundImage } = useMutation({
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ import ImageCustomization from "./Customization/ImageCustomization";
|
||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { deleteImage, uploadImage } from "../../api/uploadApi";
|
import { deleteImage, uploadImage } from "../../api/uploadApi";
|
||||||
import { createProject } from "../../api/projectApi";
|
import { createProject, getProjectById, updateProject } from "../../api/projectApi";
|
||||||
import { useToast } from "../../hooks/use-toast";
|
import { useToast } from "../../hooks/use-toast";
|
||||||
|
|
||||||
const UploadImage = () => {
|
const UploadImage = () => {
|
||||||
|
|
@ -56,6 +56,12 @@ const UploadImage = () => {
|
||||||
const createEmptyProject = async () => {
|
const createEmptyProject = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await createProject();
|
const response = await createProject();
|
||||||
|
if (response?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: response?.status,
|
||||||
|
description: response?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
if (response?.data?.id) {
|
if (response?.data?.id) {
|
||||||
navigate(`/${response.data.id}`);
|
navigate(`/${response.data.id}`);
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +72,38 @@ const UploadImage = () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
createEmptyProject();
|
createEmptyProject();
|
||||||
}
|
}
|
||||||
}, [id, navigate]);
|
}, [id, navigate, toast]);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// to get each canvas_project data
|
||||||
|
const { data: projectData } = useQuery({
|
||||||
|
queryKey: ['project', id],
|
||||||
|
queryFn: async () => await getProjectById(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(projectData);
|
||||||
|
|
||||||
|
// to update the project
|
||||||
|
const { mutate: projectUpdate } = useMutation({
|
||||||
|
mutationFn: async ({ id, updateData }) => {
|
||||||
|
return await updateProject({ id, ...updateData })
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
// Invalidate a single query key
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message,
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// upload image handler
|
// upload image handler
|
||||||
const { mutate } = useMutation({
|
const { mutate } = useMutation({
|
||||||
|
|
@ -94,6 +131,12 @@ const UploadImage = () => {
|
||||||
},
|
},
|
||||||
{ crossOrigin: "anonymous" }
|
{ crossOrigin: "anonymous" }
|
||||||
);
|
);
|
||||||
|
if (canvas) {
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||||
|
const updateData = { ...projectData?.data, object, preview_url: "" };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
projectUpdate({ id, updateData });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -104,7 +147,7 @@ const UploadImage = () => {
|
||||||
removeFile();
|
removeFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// handle image remove
|
// handle image remove
|
||||||
const { mutate: deleteMutate } = useMutation({
|
const { mutate: deleteMutate } = useMutation({
|
||||||
|
|
@ -117,6 +160,12 @@ const UploadImage = () => {
|
||||||
title: data?.status,
|
title: data?.status,
|
||||||
description: data?.message
|
description: data?.message
|
||||||
})
|
})
|
||||||
|
if (canvas) {
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||||
|
const updateData = { ...projectData?.data, object };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
projectUpdate({ id, updateData });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast({
|
toast({
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import PositionPanel from "./PositionPanel";
|
||||||
import ImagePanel from "./ImagePanel";
|
import ImagePanel from "./ImagePanel";
|
||||||
import GroupObjectPanel from "./GroupObjectPanel";
|
import GroupObjectPanel from "./GroupObjectPanel";
|
||||||
import CanvasPanel from "./CanvasPanel";
|
import CanvasPanel from "./CanvasPanel";
|
||||||
|
import { ProjectPanel } from "./ProjectPanel";
|
||||||
|
import ImageLibrary from "./ImageLibrary";
|
||||||
|
|
||||||
const EditorPanel = () => {
|
const EditorPanel = () => {
|
||||||
const { selectedPanel } = useContext(CanvasContext);
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
|
|
@ -42,6 +44,10 @@ const EditorPanel = () => {
|
||||||
return <GroupObjectPanel />;
|
return <GroupObjectPanel />;
|
||||||
case "canvas":
|
case "canvas":
|
||||||
return <CanvasPanel />;
|
return <CanvasPanel />;
|
||||||
|
case "project":
|
||||||
|
return <ProjectPanel />;
|
||||||
|
case "image":
|
||||||
|
return <ImageLibrary />;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/components/Panel/ImageLibrary.jsx
Normal file
28
src/components/Panel/ImageLibrary.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
|
const ImageLibrary = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Image Library</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
{/* Image library content goes here */}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageLibrary
|
||||||
119
src/components/Panel/ProjectPanel.jsx
Normal file
119
src/components/Panel/ProjectPanel.jsx
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Loader, Trash, X } from 'lucide-react';
|
||||||
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { deleteProject, getProjects } from '@/api/projectApi';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from '@/hooks/use-toast';
|
||||||
|
import ActiveObjectContext from '../Context/activeObject/ObjectContext';
|
||||||
|
import { FixedSizeList as List } from 'react-window'; // Import FixedSizeList from react-window
|
||||||
|
|
||||||
|
export const ProjectPanel = () => {
|
||||||
|
const { setSelectedPanel, canvas } = useContext(CanvasContext);
|
||||||
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient(); // Initialize query client
|
||||||
|
|
||||||
|
const { data: projects, isLoading: projectLoading, isSuccess: projectSuccess } = useQuery({
|
||||||
|
queryKey: ['projects'],
|
||||||
|
queryFn: getProjects, // Simplified function call
|
||||||
|
});
|
||||||
|
|
||||||
|
// To delete a project
|
||||||
|
const { mutate: projectDelete, isPending: deletePending } = useMutation({
|
||||||
|
mutationFn: async (id) => await deleteProject(id),
|
||||||
|
onSuccess: (data, id) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate queries to refresh data
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||||
|
|
||||||
|
// Clear canvas if it exists
|
||||||
|
if (canvas) {
|
||||||
|
canvas.clear();
|
||||||
|
canvas.renderAll();
|
||||||
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||||
|
}
|
||||||
|
setActiveObject(null);
|
||||||
|
|
||||||
|
setSelectedPanel("");
|
||||||
|
navigate("/");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to delete the project",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Row renderer for react-window
|
||||||
|
const Row = ({ index, style }) => {
|
||||||
|
const project = projects?.data?.[index]; // Get project by index
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={project?.id} className='flex flex-col gap-1 bg-red-50 p-1 rounded-md' style={style}>
|
||||||
|
<div className='rounded-md flex p-1 flex-col gap-1 hover:bg-red-100 cursor-pointer transition-all' onClick={() => navigate(`/${project.id}`)}>
|
||||||
|
<p className='font-bold text-sm truncate'>{project?.name}</p>
|
||||||
|
<p className='text-xs truncate'>{project?.description} </p>
|
||||||
|
<img className='rounded-md' src={project?.preview_url} alt="each_project" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={deletePending}
|
||||||
|
className="w-fit p-1 ml-auto" size="small" onClick={() => { projectDelete(project?.id) }}><Trash className="h-4 w-4" /></Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Projects</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
{
|
||||||
|
projectLoading && <p><Loader className="animate-spin mx-auto" /></p>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!projectLoading && projectSuccess && projects?.status === 200 &&
|
||||||
|
<List
|
||||||
|
height={500} // Set the height of the viewport
|
||||||
|
itemCount={projects?.data?.length} // Total number of items
|
||||||
|
itemSize={300} // Height of each row
|
||||||
|
width="100%" // Full width of the container
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
projects?.status !== 200 && <p>{projects?.message}</p>
|
||||||
|
}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -49,16 +49,18 @@ const SaveCanvas = () => {
|
||||||
title: data?.status,
|
title: data?.status,
|
||||||
description: data?.message,
|
description: data?.message,
|
||||||
})
|
})
|
||||||
console.log(data);
|
|
||||||
// Invalidate a single query key
|
// Invalidate a single query key
|
||||||
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||||
|
|
||||||
// to load the object into canvas
|
// Clear canvas if it exists
|
||||||
// canvas.loadFromJSON(data?.data?.object, () => {
|
if (canvas) {
|
||||||
// canvas.renderAll();
|
canvas.clear();
|
||||||
// }, (err) => {
|
canvas.renderAll();
|
||||||
// console.error('Error loading object:', err);
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||||
// });
|
}
|
||||||
|
setActiveObject(null);
|
||||||
|
setSelectedPanel("");
|
||||||
|
navigate("/");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -81,15 +83,19 @@ const SaveCanvas = () => {
|
||||||
title: data?.status,
|
title: data?.status,
|
||||||
description: data?.message,
|
description: data?.message,
|
||||||
})
|
})
|
||||||
|
// Clear canvas if it exists
|
||||||
|
if (canvas) {
|
||||||
|
canvas.clear();
|
||||||
|
canvas.renderAll();
|
||||||
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||||
|
}
|
||||||
|
setActiveObject(null);
|
||||||
|
|
||||||
// Invalidate a single query key
|
// Invalidate a single query key
|
||||||
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||||
setSelectedPanel("");
|
setSelectedPanel("");
|
||||||
navigate("/");
|
navigate("/");
|
||||||
// for clear canvas
|
|
||||||
canvas.clear();
|
|
||||||
canvas.renderAll();
|
|
||||||
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
|
||||||
setActiveObject(null);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -179,19 +185,14 @@ const SaveCanvas = () => {
|
||||||
if (saveCanvas?.preview_url) {
|
if (saveCanvas?.preview_url) {
|
||||||
try {
|
try {
|
||||||
removeCanvasImage(saveCanvas?.preview_url);
|
removeCanvasImage(saveCanvas?.preview_url);
|
||||||
console.log("Image removed", saveCanvas?.preview_url);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error removing image:", error);
|
console.error("Error removing image:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
console.log("Capturing image...");
|
|
||||||
const file = await captureImage();
|
const file = await captureImage();
|
||||||
console.log("Captured file:", file);
|
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
uploadCanvasImage({ file, id });
|
uploadCanvasImage({ file, id });
|
||||||
console.log("Image uploaded successfully");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error capturing image:", error);
|
console.error("Error capturing image:", error);
|
||||||
|
|
@ -270,13 +271,18 @@ const SaveCanvas = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='my-2'>
|
<div className='my-2'>
|
||||||
<h2 className='font-bold text-sm flex gap-2'><Save /> Save Canvas </h2>
|
<h2 className='font-bold text-sm flex gap-2 my-4'><Save /> Save Canvas </h2>
|
||||||
|
|
||||||
|
{
|
||||||
|
saveCanvas?.preview_url &&
|
||||||
<div className='bg-red-50 p-2 my-2'>
|
<div className='bg-red-50 p-2 my-2'>
|
||||||
{
|
{
|
||||||
saveCanvas?.preview_url &&
|
saveCanvas?.preview_url &&
|
||||||
<img src={saveCanvas?.preview_url} alt="" className='rounded-md shadow-sm' />
|
<img src={saveCanvas?.preview_url} alt="" className='rounded-md shadow-sm' />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Name:</Label>
|
<Label>Name:</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue