Init Commit

This commit is contained in:
KarimTamani
2025-05-04 19:17:04 +01:00
commit d9905351bd
61 changed files with 3015 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
import { Route, Routes } from "react-router-dom";
import "@/styles/globals.css"
import { TooltipProvider } from "./components/tooltip/tooltip";
import { ReactFlowProvider } from "@xyflow/react";
import useAppRoutes from "./routes/app-route";
function App() {
const appRoutes = useAppRoutes() ;
return (
<ReactFlowProvider>
<TooltipProvider delayDuration={0}>
{appRoutes}
</TooltipProvider>
</ReactFlowProvider>
);
}
export default App;
@@ -0,0 +1,56 @@
import { colorOptions } from "@/lib/colors";
import { Button, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { useCallback, useState } from "react";
interface ColorPickerProps {
defaultColor?: string;
onChange?: (color: string) => void
}
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
const [color , setColor] = useState<string>( defaultColor as string ) ;
const [isOpen, setIsOpen] = useState(false);
const onSelect = useCallback((color: string) => {
setIsOpen(false);
onChange && onChange(color) ;
setColor ( color)
}, [onChange])
return (
<Popover placement="bottom" radius="sm" isOpen={isOpen} onOpenChange={(open) => setIsOpen(open)}>
<PopoverTrigger>
<Button size="sm"
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
isIconOnly
style={{
backgroundColor : color
}}
>
</Button>
</PopoverTrigger>
<PopoverContent>
<div className="grid grid-cols-4 gap-2">
{colorOptions.map(color => (
<div
key={color}
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
style={{
backgroundColor: color
}}
onClick={() => onSelect(color)}
>
</div>
))}
</div>
</PopoverContent>
</Popover>
)
}
export default ColorPicker;
+90
View File
@@ -0,0 +1,90 @@
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button, useDisclosure, Popover, PopoverTrigger, PopoverContent, Divider, DropdownSection } from "@heroui/react";
import { useMemo, useState } from "react";
import SubmenuDropdown from "./submenu-dropdown";
export interface MenuDropdownProps {
title?: string;
children?: MenuDropdownProps[];
theme?: "default" | "danger",
isDisabled?: boolean,
divide?: boolean,
shortcut?: string,
isOpen?: boolean
}
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, isOpen }) => {
const disabledChilds: string[] = useMemo(() => {
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled && child.title).map((child: MenuDropdownProps) => child.title as string) : []
}, [children]);
return <Dropdown radius="sm" >
{
title &&
<DropdownTrigger >
<Button size="sm" variant="light" >
<span className=" text-left flex justify-between text-small font-semibold">
{title}
</span>
</Button>
</DropdownTrigger>
}
<HeroDropdownMenu disabledKeys={disabledChilds} >
{
children ? children?.map((child: MenuDropdownProps) => (
<DropdownItem
key={child.title as string}
className={child.theme == "danger" ? "text-danger" : ""}
color={child.theme}
shortcut={child.shortcut}
>
{
!child.children ?
<div>
{child.title}
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-default-200"></div>
}
</div>
:
<div >
<SubmenuDropdown {...child} />
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-default-200"></div>
}
</div>
}
</DropdownItem>
)) : []
}
</HeroDropdownMenu>
</Dropdown>
}
export default DropdownMenu;
+117
View File
@@ -0,0 +1,117 @@
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
const menu: MenuDropdownProps[] = [
{
title: "File",
children: [{
title: "New"
},
{
title: "Open",
shortcut: "Ctnl + O"
}, {
title: "Save",
shortcut: "Ctnl + S",
divide: true
}, {
title: "Import",
divide: true,
children: [{
title: ".json",
}, {
title: ".dbml"
}, {
title: "MySql"
}, {
title: "Postgresql"
}]
}, {
title: "Export SQL",
children: [{
title: "Generic",
}, {
title: "MySql"
}, {
title: "Postgresql"
}]
}, {
title: "Export ORM Models",
divide: true
}, {
title: "Delete Project",
theme: "danger"
}]
}, {
title: "Edit",
children: [{
title: "Undo",
isDisabled: true,
},
{
title: "Redo",
isDisabled: true,
}, {
title: "Clear",
}]
} , {
title : "View" ,
children : [{
title : "Hide Controller" ,
shortcut : "Ctnl + B" ,
divide : true
} , {
title : "Zoom on scroll" ,
divide : true ,
children : [{
title : "On"
} , {
title : "Off"
}] ,
} , {
title : "Theme" ,
children : [{
title : "Light"
} , {
title : "Dark"
}]
}]
} , {
title : "Help" ,
children : [{
title : "Show Docs" ,
} , {
title : "Join Discord"
}]
}
]
interface MenuProps {
}
const Menu: React.FC<MenuProps> = ({ }) => {
return menu.map(menuItem => (
<DropdownMenu {...menuItem} />
))
}
export default Menu;
+35
View File
@@ -0,0 +1,35 @@
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
import { MenuDropdownProps } from "./menu-dropdown";
import { ChevronRight } from "lucide-react";
import DropdownMenu from "./menu-dropdown";
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ title, children }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<div onMouseEnter={onOpen} onMouseLeave={onClose} >
<Popover placement="right" isOpen={isOpen} radius="sm" offset={90} >
<PopoverTrigger>
<Button className="w-full h-6 bg-transparent p-0" size="sm" value={"light"}>
<span className="w-full text-left flex justify-between text-small font-normal">
{title}
<ChevronRight className="size-4 text-icon" />
</span>
</Button>
</PopoverTrigger>
<PopoverContent>
<DropdownMenu
children={children}
/>
</PopoverContent>
</Popover>
</div>
)
};
export default SubmenuDropdown;
+31
View File
@@ -0,0 +1,31 @@
import { NavbarBrand, NavbarContent, Navbar as NavbarContainer, Image, Dropdown, DropdownTrigger, Button, DropdownMenu, DropdownItem } from "@heroui/react";
import Menu from "../menu/menu";
interface Props {
}
const Navbar: React.FC<Props> = ({ }) => {
return (
<nav className="h-12 fixed z-50 bg-background w-full flex items-center p-4 border-b border-default-200">
<img
src="/stackrender.png"
width={22}
alt="logo"
/>
<h3 className="font-semibold ml-2 text-slate-900 text-sm">StackRender</h3>
<div className="ml-4">
<Menu/>
</div>
</nav>
)
}
export default Navbar;
+44
View File
@@ -0,0 +1,44 @@
import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { Divider } from "@heroui/react";
import React from "react";
export interface SidebarItemProps {
type?: "item" | "divider"
title?: string;
icon?: React.ReactNode;
href?: string;
isActive? : boolean
}
const SidebarItem: React.FC<SidebarItemProps> = ({ title, icon, href , type = "item" , isActive}) => {
if (type == "item")
return (
<Tooltip delayDuration={0} >
<TooltipTrigger asChild>
<Link to={href as string} className="p-2 rounded-md hover:bg-default-200 sidebar-item" data-active={isActive}>
{icon}
</Link>
</TooltipTrigger>
<TooltipContent
side="left"
align="center"
>
{title}
</TooltipContent>
</Tooltip>
)
else {
return <Divider className="bg-default-200" />
}
}
export default SidebarItem
+116
View File
@@ -0,0 +1,116 @@
import { BookOpen, EarthLock, GitCommitHorizontal, Github, Settings, Sparkles, Table, TableProperties, Waypoints, Workflow, X, Zap } from "lucide-react";
import SidebarItem, { SidebarItemProps } from "./sidebar-item";
import { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { useTranslation } from 'react-i18next';
interface SidebarProps {
children?: React.ReactNode
}
const sidebarItemClass : string = "text-gray-700 size-4 data-[active=true]:text-primary" ;
const Sidebar: React.FC<SidebarProps> = ({ }) => {
const { t , i18n } = useTranslation() ;
console.log ( i18n.language)
console.log ()
const location = useLocation() ;
const sidebarItems: SidebarItemProps[] = useMemo(() => [
{
title: t("sidebar.tables"),
icon: <TableProperties className={sidebarItemClass}></TableProperties>,
href: "/database/tables",
isActive : location.pathname.endsWith("/database/tables")
},
{
title: t("sidebar.relationships"),
icon: <Workflow className={sidebarItemClass}></Workflow>,
href: "/database/relationships",
isActive : location.pathname.endsWith("/database/relationships")
},
{
title: "AI",
icon: <Sparkles className={sidebarItemClass}></Sparkles>,
href: "/database/bot",
isActive : location.pathname.endsWith("/database/bot")
},
{
type: "divider"
},
{
title: "API",
icon: <Zap className={sidebarItemClass}></Zap>,
href: "/database/ai-bot",
},
{
title: "Authentication",
icon: <EarthLock className={sidebarItemClass}></EarthLock>,
href: "/database/ai-bot",
},
{
title: "Controllers",
icon: <Waypoints className={sidebarItemClass}></Waypoints>,
href: "/database/ai-bot",
},
{
type: "divider"
},
{
title: "Git",
icon: <GitCommitHorizontal className={sidebarItemClass}></GitCommitHorizontal>,
href: "/database/ai-bot",
},
{
type: "divider"
},
{
title: "Project Setting",
icon: <Settings className={sidebarItemClass}></Settings>,
href: "/database/ai-bot",
},
], [location])
const bottomSidebarItems: SidebarItemProps[] = useMemo(() => [
{
title: "Github",
icon: <Github className={sidebarItemClass}></Github>,
href: "/database/ai-bot",
},
{
title: "Docs",
icon: <BookOpen className={sidebarItemClass}></BookOpen>,
href: "/database/ai-bot",
},
], []) ;
return (
<aside className="h-full z-[20] flex flex-col items-between py-2 justify-between sticky top-0 duration-500 w-12 bg-sidebar border-r pt-[56px]">
<div className="flex flex-col items-center gap-2">
{
sidebarItems.map((item: SidebarItemProps) => (
<SidebarItem
{...item}
/>
))
}
</div>
<div className="flex flex-col items-center gap-2 ">
{
bottomSidebarItems.map((item: SidebarItemProps) => (
<SidebarItem
{...item}
/>
))
}
</div>
</aside>
)
}
export default Sidebar;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@heroui/react';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+40
View File
@@ -0,0 +1,40 @@
import { en, enLanguage } from "./languages/en";
import { fr, frLanguage } from "./languages/fr";
import { Language } from "./types";
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { ar, arLanguage } from "./languages/ar";
export const languages: Language[] = [
enLanguage,
frLanguage ,
arLanguage
];
const resources = {
en,
fr,
ar
};
i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
resources ,
lng: 'en', // 👈 set default language to French
interpolation: {
escapeValue: false,
},
fallbackLng: enLanguage.code,
debug: false,
});
export { i18n };
+51
View File
@@ -0,0 +1,51 @@
import { Language } from "../types";
export const ar = {
translation: {
sidebar: {
tables: "الجداول",
relationships: "العلاقات"
},
db_controller: {
filter : "تصفية" ,
add_table : "إضافة جدول" ,
add_field : "إضافة حقل" ,
add_index : "إضافة فهرس" ,
add_relationship : "إضافة علاقة" ,
show_code : "عرض الكود" ,
fields : "الحقول" ,
indexes : "الفهارس" ,
note : "ملاحظة" ,
name : "الاسم" ,
type : "النوع" ,
primary_key : "المفتاح الأساسي" ,
nullable : "يسمح بالقيمة الفارغة" ,
select_fields : "اختر الحقول" ,
unique : "فريد" ,
table_note : "ملاحظة الجدول" ,
collapse : "إخفاء الكل" ,
primary_table : "الجدول الأساسي" ,
referenced_table : "الجدول المشار إليه" ,
cardinality :{
name : "العلاقة" ,
one_to_one : "واحد إلى واحد" ,
one_to_many : "واحد إلى متعدد" ,
many_to_one : "متعدد إلى واحد" ,
many_to_many : "متعدد إلى متعدد" ,
},
delete : "حذف" ,
} ,
table: {
double_click: "انقر مرتين للتعديل"
},
}
}
export const arLanguage: Language = {
name: 'Arabic',
nativeName: 'العربية',
code: 'ar',
};
+52
View File
@@ -0,0 +1,52 @@
import { Language } from "../types";
export const en = {
translation : {
sidebar : {
tables : "Tables" ,
relationships : "Relationships"
} ,
db_controller : {
filter : "Filter" ,
add_table : "Add Table" ,
add_field : "Add Field" ,
add_index : "Add Index" ,
add_relationship : "Add Relationship" ,
show_code : "Show code" ,
fields : "Fields" ,
indexes : "Indexes" ,
note : "Note" ,
name : "Name" ,
type : "Type" ,
primary_key : "Primary Key" ,
nullable : "Nullable" ,
select_fields : "Select fields" ,
unique : "Unique" ,
table_note : "Table note" ,
collapse : "Collapse All" ,
primary_table : "Primary Table" ,
referenced_table : "Referenced Table" ,
cardinality :{
name : "Cardinality" ,
one_to_one : "One to One" ,
one_to_many : "One to Many" ,
many_to_one : "Many to One" ,
many_to_many : "Many to Many" ,
} ,
delete : "Delete" ,
},
table : {
double_click : "Double click to edit"
}
}
}
export const enLanguage : Language = {
name : "English" ,
nativeName: 'English',
code: 'en',
}
+51
View File
@@ -0,0 +1,51 @@
import { Language } from "../types";
export const fr = {
translation: {
sidebar: {
tables: "Tableaux",
relationships: "Relations"
} ,
db_controller : {
filter : "Filtrer" ,
add_table : "Ajouter une table" ,
add_field : "Ajouter un champ" ,
add_index : "Ajouter un index" ,
add_relationship : "Ajouter une relation" ,
show_code : "Afficher le code" ,
fields : "Champs" ,
indexes : "Index" ,
note : "Note" ,
name : "Nom" ,
type : "Type" ,
primary_key : "Clé primaire" ,
nullable : "Nullable" ,
select_fields : "Sélectionner des champs" ,
unique : "Unique" ,
table_note : "Note de la table" ,
collapse : "Tout réduire" ,
primary_table : "Table primaire" ,
referenced_table : "Table référencée" ,
cardinality :{
name : "Cardinalité" ,
one_to_one : "Un à un" ,
one_to_many : "Un à plusieurs" ,
many_to_one : "Plusieurs à un" ,
many_to_many : "Plusieurs à plusieurs" ,
},
delete : "Supprimer" ,
} ,
table: {
double_click: "Double-cliquez pour modifier"
},
}
}
export const frLanguage: Language = {
name: "French",
nativeName: 'Français',
code: 'fr',
}
+7
View File
@@ -0,0 +1,7 @@
export type Language = {
name: string;
nativeName: string;
code: string;
};
+6
View File
@@ -0,0 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+21
View File
@@ -0,0 +1,21 @@
export const colorOptions = [
"#fd7f6f", // Warm coral red
"#7eb0d5", // Soft sky blue
"#b2e061", // Fresh lime green
"#bd7ebe", // Soft lavender pink
"#ffb55a", // Vibrant orange
"#ffee65", // Warm yellow
"#beb9db", // Light purple grey
"#fdcce5", // Light blush pink
"#8bd3c7", // Minty teal
"#f4a1a1", // Soft peachy red
"#9bd78c", // Fresh pastel green
"#d1a9e0" // Light lilac purple
];
export const randomColor = () => {
return colorOptions[Math.floor(Math.random() * colorOptions.length)];
};
+4
View File
@@ -0,0 +1,4 @@
export interface DataType {
id: string;
name: string;
}
+14
View File
@@ -0,0 +1,14 @@
import { DataType } from "../data/data-types";
export interface Field {
id: string;
name: string;
type: DataType;
primaryKey: boolean;
unique: boolean;
nullable: boolean;
createdAt: number;
default?: string;
}
+22
View File
@@ -0,0 +1,22 @@
export interface Relationship {
id: string;
name: string;
sourceSchema?: string;
sourceTableId: string;
targetSchema?: string;
targetTableId: string;
sourceFieldId: string;
targetFieldId: string;
sourceCardinality: Cardinality;
targetCardinality: Cardinality;
createdAt: number;
}
export type RelationshipType =
| 'one_to_one'
| 'one_to_many'
| 'many_to_one'
| 'many_to_many';
export type Cardinality = 'one' | 'many';
+14
View File
@@ -0,0 +1,14 @@
import { Field } from "./field";
export interface Table {
id: string;
name: string;
x: number;
y: number;
fields: Field[];
width?: number;
order?: number;
color: string;
createdAt: number;
comments?: string;
}
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App.tsx";
import { Provider } from "./provider.tsx";
import "./index.css";
import "./i18/index.ts" ;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<Provider>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>,
);
+27
View File
@@ -0,0 +1,27 @@
import Sidebar from "@/components/sidebar/sidebar";
import DatabasePage from "../database/database-page";
import Navbar from "@/components/navbar/navbar";
import { Outlet } from "react-router-dom";
interface Props {
}
const DashboardPage: React.FC<Props> = ({ }) => {
return (
<div className="flex flex-col justify-between h-full h-screen">
<Navbar />
<div className="flex">
<Sidebar />
<Outlet />
</div>
</div>
)
}
export default DashboardPage;
+211
View File
@@ -0,0 +1,211 @@
import { Table as TableType } from "@/lib/interfaces/table";
import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
import { useCallback, useMemo } from "react";
import Table from "./table/table";
import '@xyflow/react/dist/style.css';
import { Relationship } from "./table/relationship";
import DBController from "./db-controller/db-controller";
interface DatabaseProps {
initialTables?: TableType
}
const initialNodes = [
{
id: '1', type: "table", position: { x: 500, y: 500 }, data: {
table: {
id: "1",
name: "users",
fields: [
{
id: 'e5n46eojbyxpg65pb1sesysi2',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: '7nbmg2bt6dzltr8rfouxctdlc',
name: 'reservation_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'f9kcpflp010xe5ad3rmexla63',
name: 'rating',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: '3znekob0pqusyvdoq14nhlvgo',
name: 'comment',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
}
},
style: { width: 224 }
},
{
id: '2', type: "table", position: { x: 200, y: 500 }, data: {
table: {
id: "1",
name: "products",
fields: [
{
id: 'e5n46eojbyxpg65pb1sesysi2',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: '7nbmg2bt6dzltr8rfouxctdlc',
name: 'user_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'f9kcpflp010xe5ad3rmexla63',
name: 'rating',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'f9kcpflp010xe5ad3rmexla63',
name: 'rating',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'f9kcpflp010xe5ad3rmexla63',
name: 'rating',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: '3znekob0pqusyvdoq14nhlvgo',
name: 'comment',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
}
},
style: { width: 224 }
},
];
const initialEdges: any[] = [];
const edgeTypes = {
'relationship-edge': Relationship,
};
const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const nodeTypes = useMemo(() => ({ table: Table }), []);
const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<div className="w-full h-screen flex">
<DBController/>
<ReactFlow
nodes={nodes}
edges={edges}
fitView
className="w-full h-full"
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
defaultEdgeOptions={{
animated: false,
type: 'relationship-edge',
}}
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
>
<Controls />
<Background className="bg-default/10" />
</ReactFlow>
</div>
)
}
export default DatabasePage;
@@ -0,0 +1,20 @@
import { Outlet } from "react-router-dom"
interface Props {
}
const DBController : React.FC<Props> = ({ }) => {
return(
<div className="w-[640px] h-full bg-background border-r p-2 pt-[52px] ">
<Outlet/>
</div>
)
}
export default DBController
@@ -0,0 +1,105 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Button, Select, SelectItem } from "@heroui/react";
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, SquareArrowLeft, SquareArrowRight, Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
interface Props {
}
export const cardinalities = [
{ key: "one2one", label: "One to One" },
{ key: "one2many", label: "One to Many" },
{ key: "many2one", label: "Many to One" },
{ key: "many2many", label: "Many 2 many" },
];
const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
const {t } = useTranslation() ;
return (
<div className="w-full p-2 space-y-4">
<div className="flex">
<div className="w-full space-y-1">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileOutput className="size-4" />
{t("db_controller.primary_table")}
</label>
<Tooltip>
<TooltipTrigger>
<span className="truncate text-left text-sm">
users(id)
</span>
</TooltipTrigger>
<TooltipContent>
users(id)
</TooltipContent>
</Tooltip>
</div>
<div className="w-full space-y-1">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileMinus2 className="size-4" />
{t("db_controller.referenced_table")}
</label>
<Tooltip>
<TooltipTrigger>
<span className="truncate text-left text-sm ">
products(user_id)
</span>
</TooltipTrigger>
<TooltipContent>
products(user_id)
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="space-y-2">
<label className="flex text-sm font-medium flex text-slate-700 items-center gap-1">
<ChevronsLeftRightEllipsis className="size-4 " />
{t('db_controller.cardinality.name')}
</label>
<Select
className="w-full"
size="sm"
variant="bordered"
>
<SelectItem key={"one2one"}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
<SelectItem key={"one2many"}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
<SelectItem key={"many2one"}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
<SelectItem key={"many2many"}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
</Select>
</div>
<div className="flex justify-center">
<Button
variant="solid"
className="bg-transparent text-xs"
radius="sm"
size="sm"
>
<Trash2 className="mr-1 size-3.5 text-danger" />
<div className="text-danger font-semibold">
{t("db_controller.delete")}
</div>
</Button>
</div>
</div>
)
}
export default RelationshipAccordionBody;
@@ -0,0 +1,90 @@
import { randomColor } from "@/lib/colors";
import { Button, cn, Input } from "@heroui/react";
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil } from "lucide-react";
import { useState } from "react";
interface RelationshipAccordionHeaderProps {
isOpen?: boolean;
}
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen }) => {
const [editMode, setEditMode] = useState<boolean>(false);
return (
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
<div className={cn(
'tarnsition-all duration-200',
isOpen ? "rotate-[90deg]" : ""
)}>
<ChevronRight className="size-4 text-icon" />
</div>
{
editMode && <>
<Input
placeholder={"Usres"}
autoFocus
size="sm"
variant="bordered"
onBlur={() => setEditMode(false)}
type="text"
className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm "
/>
<Button
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
size="sm"
onPress={() => setEditMode(false)}
isIconOnly
>
<Check className="size-4 text-icon" />
</Button>
</>
}
{
!editMode && <>
<label
className="w-full truncate px-2 py-1 text-sm font-semibold text-black"
>
users one or many posts
</label>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
size="sm"
isIconOnly
variant="light"
>
<Focus className="size-4 text-icon" />
</Button>
<Button
size="sm"
isIconOnly
variant="light"
onPress={() => setEditMode(true)}
>
<Pencil className="size-4 text-icon" />
</Button>
</div>
<Button
size="sm"
isIconOnly
variant="light"
>
<EllipsisVertical className="size-4 text-slate-500" />
</Button>
</>
}
</div>
)
}
export default RelationshipAccordionHeader;
@@ -0,0 +1,104 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
import { Code, List, ListCollapse, Table, Workflow } from "lucide-react"
import { useState } from "react";
import { useTranslation } from "react-i18next";
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
interface Props {
}
const RelationshipController: React.FC<Props> = ({ }) => {
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
const { t } = useTranslation();
return (
<div className="w-full h-full flex flex-col gap-2">
<div className="flex items-center justify-between gap-4 py-1">
<div>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="light"
className="size-8 p-0"
isIconOnly
onPress={() =>
//setShowDBML((value) => !value)
console.log("hello world ")
}
>
<ListCollapse className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.collapse")}
</TooltipContent>
</Tooltip>
</div>
<div className="flex-1">
<Input
//ref={filterInputRef}
type="text"
size="sm"
radius="sm"
variant="bordered"
placeholder={t("db_controller.filter")}
//placeholder={t('side_panel.tables_section.filter')}
className="h-8 w-full focus-visible:ring-0 "
//value={filterText}
//onChange={(e) => setFilterText(e.target.value)}
/>
</div>
<Button
variant="solid"
radius="sm"
color="primary"
startContent={
<Workflow className="h-4 w-4 " />
}
className="h-8 p-2 text-xs font-semibold "
//onClick={handleCreateTable}
>{t("db_controller.add_relationship")}
</Button>
</div>
<div className=" flex-1 overflow-auto">
<Accordion
hideIndicator
dividerProps={{
className : "bg-default-200"
}}
>
{items.map(item => (
<AccordionItem
key={item}
aria-label={item}
classNames={{
trigger: "w-full hover:bg-default transition-all duration-200",
base: "rounded-md p-0 overflow-hidden",
}}
subtitle={
<RelationshipAccordionHeader/>
}
>
<RelationshipAccordionBody/>
</AccordionItem>
))}
</Accordion>
</div>
</div>
)
}
export default RelationshipController
@@ -0,0 +1,116 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { useSortable } from "@dnd-kit/sortable";
import { Autocomplete, AutocompleteItem, Button, Input } from "@heroui/react";
import { EllipsisVertical, Grip, GripVertical, KeyRound } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
interface Props {
id : string
}
export const animals = [
{ label: "Cat", key: "cat", description: "The second most popular pet in the world" },
{ label: "Dog", key: "dog", description: "The most popular pet in the world" },
{ label: "Elephant", key: "elephant", description: "The largest land animal" },
{ label: "Lion", key: "lion", description: "The king of the jungle" },
{ label: "Tiger", key: "tiger", description: "The largest cat species" },
{ label: "Giraffe", key: "giraffe", description: "The tallest land animal" },
{
label: "Dolphin",
key: "dolphin",
description: "A widely distributed and diverse group of aquatic mammals",
},
{ label: "Penguin", key: "penguin", description: "A group of aquatic flightless birds" },
{ label: "Zebra", key: "zebra", description: "A several species of African equids" },
{
label: "Shark",
key: "shark",
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
},
{
label: "Whale",
key: "whale",
description: "Diverse group of fully aquatic placental marine mammals",
},
{ label: "Otter", key: "otter", description: "A carnivorous mammal in the subfamily Lutrinae" },
{ label: "Crocodile", key: "crocodile", description: "A large semiaquatic reptile" },
];
const FieldItem: React.FC<Props> = ({id }) => {
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
};
return (
<div className="flex w-full gap-1 items-center " style={style} ref={setNodeRef} {...attributes}>
<div {...listeners}>
<GripVertical className="size-4 text-icon" />
</div>
<Input
variant="bordered"
size="sm"
placeholder={t("db_controller.name")}
value={id}
/>
<Autocomplete
className="w-full"
defaultItems={animals}
size="sm"
variant="bordered"
placeholder={t("db_controller.type")}
>
{(item) => <AutocompleteItem key={item.key}>{item.label}</AutocompleteItem>}
</Autocomplete>
<div className="flex gap-2 ml-2">
<Tooltip>
<TooltipTrigger asChild>
<button className="p-1 px-3 transition-all hover:bg-default rounded duration-200" >
<span className="text-icon text-sm">
N
</span>
</button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.nullable")}?
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button className="p-1 px-2 transition-all hover:bg-default rounded duration-200" >
<span className="text-icon text-sm">
<KeyRound className="size-4" />
</span>
</button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.primary_key")}
</TooltipContent>
</Tooltip>
<Button
size="sm"
isIconOnly
variant="light"
>
<EllipsisVertical className="size-4 text-icon" />
</Button>
</div>
</div>
)
}
export default FieldItem;
@@ -0,0 +1,72 @@
import { Button } from "@heroui/react";
import { Plus } from "lucide-react";
import FieldItem from "./field-item";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
interface Props {
}
const FieldList: React.FC<Props> = ({ }) => {
const { t } = useTranslation();
const [fields, setFields] = useState<string[]>(["Field 1", "Field 2", "Field 3", "Field 4"])
const sensors = useSensors(
useSensor(PointerSensor)
);
function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over?.id) {
setFields((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}
return (
<div className="w-full space-y-2 no-select">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<div className="space-y-2">
<SortableContext
items={fields}
strategy={verticalListSortingStrategy}
>
{
fields.map((field: string) => (
<FieldItem id={field}/>
))
}
</SortableContext>
</div>
</DndContext>
<Button
variant="flat"
radius="sm"
startContent={
<Plus className="size-4 text-icon" />
}
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-gray font-semibold"
//onClick={handleCreateTable}
>
{t("db_controller.add_field")}
</Button>
</div >
)
}
export default FieldList;
@@ -0,0 +1,73 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Button, Select, SelectItem } from "@heroui/react";
import { EllipsisVertical } from "lucide-react";
import { useTranslation } from "react-i18next";
export const animals = [
{ key: "cat", label: "Cat" },
{ key: "dog", label: "Dog" },
{ key: "elephant", label: "Elephant" },
{ key: "lion", label: "Lion" },
{ key: "tiger", label: "Tiger" },
{ key: "giraffe", label: "Giraffe" },
{ key: "dolphin", label: "Dolphin" },
{ key: "penguin", label: "Penguin" },
{ key: "zebra", label: "Zebra" },
{ key: "shark", label: "Shark" },
{ key: "whale", label: "Whale" },
{ key: "otter", label: "Otter" },
{ key: "crocodile", label: "Crocodile" },
];
interface Props {
}
const IndexItem: React.FC<Props> = ({ }) => {
const { t } = useTranslation();
return (
<div className="flex gap-2 w-full">
<Select
className="w-full"
placeholder={t("db_controller.select_fields")}
selectionMode="multiple"
size="sm"
variant="bordered"
>
{animals.map((animal) => (
<SelectItem key={animal.key}>{animal.label}</SelectItem>
))}
</Select>
<div className="flex gap-2 ml-2">
<Tooltip>
<TooltipTrigger asChild>
<button className="p-1 px-3 transition-all hover:bg-default rounded duration-200" >
<span className="text-icon text-sm">
U
</span>
</button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.unique")}?
</TooltipContent>
</Tooltip>
<Button
size="sm"
isIconOnly
variant="light"
>
<EllipsisVertical className="size-4 text-icon" />
</Button>
</div>
</div>
)
}
export default IndexItem;
@@ -0,0 +1,36 @@
import { Button, Select, SelectItem } from "@heroui/react";
import IndexItem from "./index-item";
import { Plus } from "lucide-react";
import { useTranslation } from "react-i18next";
interface Props {
}
const IndexesList: React.FC<Props> = ({ }) => {
const { t} = useTranslation() ;
return (
<div className="w-full flex-col space-y-2">
<div>
<IndexItem />
</div>
<Button
variant="flat"
radius="sm"
startContent={
<Plus className="size-4 text-icon" />
}
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-gray font-semibold"
//onClick={handleCreateTable}
>
{t("db_controller.add_index")}
</Button>
</div>
)
}
export default IndexesList;
@@ -0,0 +1,137 @@
import { Table } from "@/lib/interfaces/table"
import { Accordion, AccordionItem, Button, cn, Divider, Textarea } from "@heroui/react";
import { ChevronLeft, ChevronRight, FileKey, FileText, FileType, Key, MessageSquareQuote } from "lucide-react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import ColorPicker from "@/components/color-picker/color-picker";
import FieldList from "./field/field-list";
import IndexesList from "./index/indexes-list";
export interface TableAccordionBodyProps {
table?: Table ,
}
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
const { t } = useTranslation();
const onColorChange = useCallback((color : string) => {
console.log ( color)
} ,[])
return (
<div className="w-full">
<Accordion
variant={"light"} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys as any}>
<AccordionItem key="fields" aria-label="Fields"
indicator={({ isOpen }) => (
<div className={cn(
'tarnsition-all duration-200',
isOpen ? "rotate-[-90deg]" : ""
)}>
<ChevronLeft className="size-4 text-icon" />
</div>
)}
classNames={{
trigger: "hover:bg-default h-6 "
}}
subtitle={
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
<FileType className="size-4 " />
<label className="text-sm w-full cursor-pointer">
{t("db_controller.fields")}
</label>
</div>
}
>
<FieldList />
</AccordionItem>
<AccordionItem key="indexes" aria-label="Indexes"
indicator={({ isOpen }) => (
<div className={cn(
'tarnsition-all duration-200',
isOpen ? "rotate-[-90deg]" : ""
)}>
<ChevronLeft className="size-4 text-icon" />
</div>
)}
classNames={{
trigger: "hover:bg-default"
}}
subtitle={
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
<FileKey className="size-4 " />
<label className="text-sm w-full cursor-pointer">
{t("db_controller.indexes")}
</label>
</div>
}>
<IndexesList/>
</AccordionItem>
<AccordionItem key="note" aria-label="Note"
indicator={({ isOpen }) => (
<div className={cn(
'tarnsition-all duration-200',
isOpen ? "rotate-[-90deg]" : ""
)}>
<ChevronLeft className="size-4 text-icon" />
</div>
)}
classNames={{
trigger: "hover:bg-default"
}}
subtitle={
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
<MessageSquareQuote className="size-4" />
<label className="text-sm w-full cursor-pointer">
{t("db_controller.note")}
</label>
</div>
}>
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")} classNames={{
inputWrapper: "bg-default/80",
}} />
</AccordionItem>
</Accordion>
<hr className="bg-default-200" />
<div className="flex p-2 pb-0 items-start">
<div className="w-full">
<ColorPicker
onChange={onColorChange}
/>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
className="font-semibold p-4"
startContent={
<FileKey className="size-4 " />
}
>
{t("db_controller.add_index")}
</Button>
<Button
variant="ghost"
size="sm"
className="font-semibold p-4"
startContent={
<FileType className="size-4 " />
}
>
{t("db_controller.add_field")}
</Button>
</div>
</div>
</div>
)
}
export default TableAccordionBody;
@@ -0,0 +1,112 @@
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
import { randomColor } from "@/lib/colors";
import { Table } from "@/lib/interfaces/table"
import { useSortable } from "@dnd-kit/sortable";
import { Button, cn, Divider, Input } from "@heroui/react";
import { Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, EllipsisVertical, Focus, Grip, GripVertical, Pencil } from "lucide-react";
import { useState } from "react";
import { CSS } from "@dnd-kit/utilities";
import { useTranslation } from "react-i18next";
export interface TableAccordionHeaderProps {
table?: Table,
isOpen?: boolean,
id: string
}
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen, id }) => {
const style = {
borderLeft: "6px solid " + randomColor(),
};
const { t } = useTranslation();
const [editMode, setEditMode] = useState<boolean>(false);
return (
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center"
style={style}
>
<div className={cn(
'tarnsition-all duration-200',
isOpen ? "rotate-[90deg]" : ""
)}>
<ChevronRight className="size-4 text-icon" />
</div>
<div className=" w-[1px] h-full bg-default-200">
</div>
{
!editMode &&
<div className="w-full px-1">
<Tooltip>
<TooltipTrigger asChild>
<label
className="w-full text-editable truncate px-2 py-1 text-sm font-semibold text-black"
onDoubleClick={() => setEditMode(true)}
>
{id}
</label>
</TooltipTrigger>
<TooltipContent>
{t("table.double_click")}
</TooltipContent>
</Tooltip>
</div>
}
{
editMode && <>
<Input
placeholder={"Usres"}
autoFocus
size="sm"
variant="bordered"
onBlur={() => setEditMode(false)}
type="text"
className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm "
/>
<Button
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
size="sm"
onClick={() => setEditMode(false)}
isIconOnly
>
<Check className="size-4 text-icon" />
</Button>
</>
}
{
!editMode && <>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
size="sm"
isIconOnly
variant="light"
>
<Focus className="size-4 text-icon" />
</Button>
<Button
size="sm"
isIconOnly
variant="light"
onPress={() => setEditMode(true)}
>
<Pencil className="size-4 text-icon" />
</Button>
</div>
<Button
size="sm"
isIconOnly
variant="light"
>
<EllipsisVertical className="size-4 text-slate-500" />
</Button>
</>
}
</div>
)
}
export default TableAccordionHeader;
@@ -0,0 +1,103 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
import { ChevronDown, Code, EllipsisVertical, Focus, Grid, Grip, List, Pencil, Table } from "lucide-react"
import { useTranslation } from "react-i18next";
import { useState } from "react";
import TableAccordionHeader from "./table-accordion-item/table-accordion-header";
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
interface Props {}
const TablesController: React.FC<Props> = ({ }) => {
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
const { t } = useTranslation();
return (
<div className="w-full h-full flex flex-col gap-2">
<div className="flex items-center justify-between gap-4 py-1">
<div>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="light"
className="size-8 p-0"
isIconOnly
onPress={() =>
//setShowDBML((value) => !value)
console.log("hello world ")
}
>
{false ? (
<List className="size-4" />
) : (
<Code className="size-4" />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.show_code")}
</TooltipContent>
</Tooltip>
</div>
<div className="flex-1">
<Input
//ref={filterInputRef}
type="text"
size="sm"
radius="sm"
variant="bordered"
placeholder={t("db_controller.filter")}
className="h-8 w-full focus-visible:ring-0"
//value={filterText}
//onChange={(e) => setFilterText(e.target.value)}
/>
</div>
<Button
variant="solid"
color="primary"
radius="sm"
startContent={
<Table className="h-4 w-4 " />
}
className="h-8 p-2 text-xs font-semibold"
//onClick={handleCreateTable}
> {t("db_controller.add_table")}
</Button>
</div>
<div className=" flex-1 overflow-auto">
<Accordion
hideIndicator
variant="splitted"
>
{items.map(item => (
<AccordionItem
key={item}
aria-label={item}
classNames={{
trigger: "w-full hover:bg-default transition-all duration-200",
base: "rounded-md shadow p-0 overflow-hidden",
}}
subtitle={
<TableAccordionHeader id={item} />
}
>
<TableAccordionBody />
</AccordionItem>
))}
</Accordion>
</div>
</div>
)
}
export default TablesController
+108
View File
@@ -0,0 +1,108 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Field as FieldType } from "@/lib/interfaces/field";
import { Button, cn, select } from "@heroui/react";
import { Handle, Position, useConnection } from "@xyflow/react";
import { Check } from "lucide-react";
import React, { useState } from "react";
interface Props {
field: FieldType,
showHandles?: boolean
}
const Field: React.FC<Props> = ({ field, showHandles }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const connection = useConnection();
return (
<div className="group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 transition-all duration-200 ease-in-out ">
{
!editMode &&
<>
<label
className=" truncate text-sm "
onDoubleClick={() => setEditMode(true)}
>
{field.name}
</label>
<span className="content-center truncate text-right text-xs text-muted-foreground text-default-600" >
{field.type.name.split(' ')[0]}
</span>
</>
}
{
editMode &&
<>
<input
// ref={inputRef}
onBlur={() => setEditMode(false)}
placeholder={field.name}
autoFocus
type="text"
// value={fieldName}
onClick={(e) => e.stopPropagation()}
// onChange={(e) => setFieldName(e.target.value)}
className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
/>
<Button
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
size="sm"
isIconOnly
onPress={() => setEditMode(false)}
>
<Check className="size-4" />
</Button>
</>
}
<div className={
cn(
"absolute w-full left-0 ",
!showHandles ? "invisible" : "visible"
)} >
<Handle
type="source"
position={Position.Left}
id={"left-"+ field.id}
className="w-4 h-4 border-3 bg-primary"
/>
<Handle
type="source"
position={Position.Right}
className="w-4 h-4 border-3 bg-primary"
id={"right-"+ field.id}
/>
</div>
{
<div className={
cn(
"absolute w-full left-0 ",
!connection.inProgress ? "invisible" : "visible"
)} >
<Handle
id={`${"target"}_${field.id}`}
className={
true
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
: `!invisible`
}
position={Position.Left}
type="target"
/>
</div>
}
</div>
)
}
export default React.memo(Field);
+140
View File
@@ -0,0 +1,140 @@
import { Relationship as RelationshipType } from "@/lib/interfaces/relationship";
import { cn } from "@heroui/react";
import { Edge, EdgeProps, getSmoothStepPath, Position, useReactFlow } from "@xyflow/react";
import { useMemo } from "react";
export type RelationshipProps = Edge<{
relationship: RelationshipType,
selected?: boolean
}, 'relationship-edge'>
export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
source,
target,
selected,
data,
}) => {
const { getInternalNode, getEdge } = useReactFlow();
//const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
//const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
const sourceNode = getInternalNode(source);
const targetNode = getInternalNode(target);
const edge = getEdge(id);
const sourceHandle: 'left' | 'right' = edge?.sourceHandle?.startsWith?.(
"right-"
)
? 'right'
: 'left';
const sourceWidth = sourceNode?.measured.width ?? 0;
const sourceLeftX =
sourceHandle === 'left' ? sourceX + 3 : sourceX - sourceWidth - 10;
const sourceRightX =
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
const targetWidth = targetNode?.measured.width ?? 0;
const targetLeftX = targetX - 1;
const targetRightX = targetX + targetWidth + 10;
const { sourceSide, targetSide } = useMemo(() => {
const distances = {
leftToLeft: Math.abs(sourceLeftX - targetLeftX),
leftToRight: Math.abs(sourceLeftX - targetRightX),
rightToLeft: Math.abs(sourceRightX - targetLeftX),
rightToRight: Math.abs(sourceRightX - targetRightX),
};
const minDistance = Math.min(
distances.leftToLeft,
distances.leftToRight,
distances.rightToLeft,
distances.rightToRight
);
const minDistanceKey = Object.keys(distances).find(
(key) => distances[key as keyof typeof distances] === minDistance
) as keyof typeof distances;
switch (minDistanceKey) {
case 'leftToRight':
return { sourceSide: 'left', targetSide: 'right' };
case 'rightToLeft':
return { sourceSide: 'right', targetSide: 'left' };
case 'rightToRight':
return { sourceSide: 'right', targetSide: 'right' };
default:
return { sourceSide: 'left', targetSide: 'left' };
}
}, [sourceLeftX, sourceRightX, targetLeftX, targetRightX]);
const [edgePath] = useMemo(
() =>
getSmoothStepPath({
sourceX: sourceSide === 'left' ? sourceLeftX : sourceRightX,
sourceY,
targetX: targetSide === 'left' ? targetLeftX : targetRightX,
targetY,
borderRadius: 14,
sourcePosition:
sourceSide === 'left' ? Position.Left : Position.Right,
targetPosition:
targetSide === 'left' ? Position.Left : Position.Right,
// offset: (edgeNumber + 1) * 14,
}),
[
sourceSide,
targetSide,
sourceLeftX,
sourceRightX,
targetLeftX,
targetRightX,
sourceY,
targetY,
// edgeNumber,
]
);
return (
<>
<path
id={id}
d={edgePath}
//markerStart={`url(#${sourceMarker})`}
//markerEnd={`url(#${targetMarker})`}
fill="none"
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
])}
onClick={(e) => {
if (e.detail === 2) {
console.log("hello world");
// openRelationshipInEditor();
}
}}
/>
<path
d={edgePath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="react-flow__edge-interaction"
onClick={(e) => {
if (e.detail === 2) {
// openRelationshipInEditor();
}
}}
/>
</>)
}
+167
View File
@@ -0,0 +1,167 @@
import { Table as TableType } from "@/lib/interfaces/table";
import { Handle, Node, NodeProps, NodeResizer, Position } from "@xyflow/react";
import React, { useCallback, useState } from 'react';
import { Button, Card, CardBody, CardHeader, cn, Divider, Input } from "@heroui/react";
import {
ChevronsLeftRight,
ChevronsRightLeft,
Table2,
ChevronDown,
ChevronUp,
Check,
CircleDotDashed,
SquareDot,
SquarePlus,
SquareMinus,
Divide,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Field } from "@/lib/interfaces/field";
import FieldComponent from "./field";
export const MAX_TABLE_SIZE = 450;
export const MID_TABLE_SIZE = 337;
export const MIN_TABLE_SIZE = 224;
export const TABLE_MINIMIZED_FIELDS = 10;
export type TableProps = Node<{
table: TableType, isOverlapping?: boolean;
highlightOverlappingTables?: boolean;
}>
const Table: React.FC<NodeProps<TableProps>> = React.memo(({
selected,
dragging,
id,
data: { table, },
}) => {
const [editMode, setEditMode] = useState<boolean>(false);
return (
<div className={cn(
"w-full h-full bg-background rounded-lg entity-card border-2 noselect ",
selected
? ' border-2 border-primary-500'
: '',
)}>
<div
className="h-2 rounded-t-[6px] "
style={{ backgroundColor: "rgb(255, 107, 138)" }}
></div>
<div className="group gap-2 flex h-9 items-center justify-between bg-default px-2 dark:bg-default">
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
{
editMode && <>
<input
placeholder={table.name}
autoFocus
onBlur={() => setEditMode(false)}
type="text"
className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
/>
<Button
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
size="sm"
onClick={() => setEditMode(false)}
isIconOnly
>
<Check className="size-3" />
</Button>
</>
}
{
!editMode &&
<>
<Tooltip>
<TooltipTrigger asChild>
<label
className=" w-full text-editable truncate px-2 py-0.5 text-sm font-bold"
onDoubleClick={() => setEditMode(true)}
>
{table.name}
</label>
</TooltipTrigger>
<TooltipContent>
Double click to edit
</TooltipContent>
</Tooltip>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
variant="light"
size="sm"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
isIconOnly
>
<CircleDotDashed className="size-4" />
</Button>
<Button
variant="light"
size="sm"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
isIconOnly
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
</div>
</>
}
</div>
<div>
<div
className="transition-[max-height] duration-200 ease-in-out"
>
{table.fields.map((field: Field) => (
<FieldComponent
key={field.id}
field={field}
showHandles={selected}
/>
))}
</div>
</div>
</div>
)
});
Table.displayName = 'table';
export default Table;
/*
focused = { false}
tableNodeId={id}
field={field}
highlighted={selectedRelEdges.some(
(edge) =>
edge.data?.relationship
.sourceFieldId === field.id ||
edge.data?.relationship
.targetFieldId === field.id
)}
visible={visibleFields.includes(field)}
isConnectable={!table.isView}
*/
+21
View File
@@ -0,0 +1,21 @@
import type { NavigateOptions } from "react-router-dom";
import { HeroUIProvider } from "@heroui/system";
import { useHref, useNavigate } from "react-router-dom";
declare module "@react-types/shared" {
interface RouterConfig {
routerOptions: NavigateOptions;
}
}
export function Provider({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
return (
<HeroUIProvider navigate={navigate} useHref={useHref}>
{children}
</HeroUIProvider>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import useDashboardRoutes from './dashboard-route';
const useAppRoutes = () => {
const dashboardRoutes = useDashboardRoutes();
return (
<Routes>
{dashboardRoutes}
</Routes>
);
};
export default useAppRoutes;
+20
View File
@@ -0,0 +1,20 @@
import DashboardPage from '@/pages/dashboard/dashboard-page';
import DatabasePage from '@/pages/database/database-page';
import { Route } from 'react-router-dom';
import useDatabaseRoutes from './database-route';
const useDashboardRoutes = () => {
const databaseRoutes = useDatabaseRoutes() ;
return (
<Route path="" element={<DashboardPage />}>
{databaseRoutes}
</Route>
);
};
export default useDashboardRoutes;
+20
View File
@@ -0,0 +1,20 @@
import DashboardPage from '@/pages/dashboard/dashboard-page';
import DatabasePage from '@/pages/database/database-page';
import RelationshipController from '@/pages/database/db-controller/relationship-controller/relationship-controller';
import TablesController from '@/pages/database/db-controller/tables-controller/tables-controller';
import { Navigate, Route, Routes } from 'react-router-dom';
const useDatabaseRoutes = () => {
return (
<Route path="/database" element={<DatabasePage />} >
<Route path='tables' element={<TablesController />}></Route>
<Route path='relationships' element={<RelationshipController />}></Route>
<Route path='bot' element={<DatabasePage />}></Route>
</Route>
);
};
export default useDatabaseRoutes;
+51
View File
@@ -0,0 +1,51 @@
.react-flow__attribution {
display: none;
}
.text-editable {
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
}
.react-flow__node.selected {
transition: box-shadow 0.3s ease;
/* Add transition */
/* box-shadow: 0px 0px 15px 0px hsl(var(--heroui-secondary-50)), 0px 2px 30px 0px hsl(var(--heroui-secondary-50)), 0px 0px 1px 0px hsl(var(--heroui-secondary-50));
*/
}
.noselect {
user-select: none;
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE10+/Edge */
}
.sidebar-item[data-active="true"] svg {
color : hsl(var(--heroui-primary-900));
}
div[data-slot="input-wrapper"] {
outline : none ;
border-width: 1px;
}
div[data-slot="mainWrapper"] button[data-slot="trigger"] {
outline : none ;
border-width: 1px;
}
div[data-slot="content"] hr[role="separator"] {
display: none ;
}
+5
View File
@@ -0,0 +1,5 @@
import { SVGProps } from "react";
export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number;
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />