added sidebar
This commit is contained in:
1000
package-lock.json
generated
1000
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"antd": "^5.20.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -20,7 +23,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"build": "GENERATE_SOURCEMAP=false env-cmd -e production react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
|
||||
45
src/App.js
45
src/App.js
@@ -1,23 +1,34 @@
|
||||
import './styles/tailwind.css'
|
||||
import logo from './assets/logo/logo.svg';
|
||||
import Sidebar from './sidebar/sidebar';
|
||||
|
||||
import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/icons";
|
||||
import WidgetsContainer from './sidebar/widgetsContainer';
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "Widgets",
|
||||
icon: <LayoutFilled />,
|
||||
content: <WidgetsContainer />
|
||||
},
|
||||
{
|
||||
name: "Extensions",
|
||||
icon: <ProductFilled />,
|
||||
content: <></>
|
||||
},
|
||||
{
|
||||
name: "Uploads",
|
||||
icon: <CloudUploadOutlined />,
|
||||
content: <></>
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="tw-w-full tw-bg-black">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
<div className="tw-w-full tw-h-[100vh] tw-flex tw-bg-primaryBg">
|
||||
|
||||
<Sidebar tabs={tabs}/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
src/assets/widgets/button.png
Normal file
BIN
src/assets/widgets/button.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
0
src/canvas/main.js
Normal file
0
src/canvas/main.js
Normal file
22
src/components/utils/draggable.js
Normal file
22
src/components/utils/draggable.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import {useDraggable} from "@dnd-kit/core"
|
||||
|
||||
|
||||
function Draggable(props) {
|
||||
const {attributes, listeners, setNodeRef, transform} = useDraggable({
|
||||
id: 'draggable',
|
||||
})
|
||||
const style = transform ? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
} : undefined
|
||||
|
||||
|
||||
return (
|
||||
<button className={`tw-bg-transparent tw-outline-none tw-border-none ${props.className}`} ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Draggable
|
||||
49
src/components/utils/widgetCard.js
Normal file
49
src/components/utils/widgetCard.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useMemo } from "react"
|
||||
import Draggable from "./draggable"
|
||||
|
||||
import { GithubOutlined, GitlabOutlined, LinkOutlined } from "@ant-design/icons"
|
||||
|
||||
|
||||
function DraggableWidgetCard({name, img, url}){
|
||||
|
||||
const urlIcon = useMemo(() => {
|
||||
if (url){
|
||||
const host = new URL(url).hostname.toLowerCase()
|
||||
|
||||
if (host === "github.com"){
|
||||
return <GithubOutlined />
|
||||
}else if(host === "gitlab.com"){
|
||||
return <GitlabOutlined />
|
||||
}else{
|
||||
return <LinkOutlined />
|
||||
}
|
||||
}
|
||||
|
||||
}, [url])
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<Draggable className="tw-cursor-pointer">
|
||||
<div className="tw-w-full tw-h-[240px] tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
|
||||
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px] tw-border-[#888] ">
|
||||
<div className="tw-h-[200px] tw-w-full tw-overflow-hidden">
|
||||
<img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
|
||||
</div>
|
||||
<span className="tw-text-xl">{name}</span>
|
||||
<div className="tw-flex tw-text-lg tw-justify-between tw-px-4">
|
||||
|
||||
<a href={url} className="tw-text-black" target="_blank" rel="noopener noreferrer">
|
||||
{urlIcon}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default DraggableWidgetCard
|
||||
0
src/components/widgets.js
Normal file
0
src/components/widgets.js
Normal file
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./styles/index.css";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
import store from "./redux/store"
|
||||
@@ -9,6 +9,8 @@ import { Provider } from "react-redux";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
|
||||
import "./styles/tailwind.css";
|
||||
import "./styles/index.css";
|
||||
|
||||
const originalSetItem = localStorage.setItem;
|
||||
// triggers itemsChaned event whenever the item in localstorage is chanegd.
|
||||
|
||||
101
src/sidebar/sidebar.js
Normal file
101
src/sidebar/sidebar.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useRef, useMemo, useState } from "react";
|
||||
|
||||
import { CloseCircleFilled } from "@ant-design/icons";
|
||||
|
||||
|
||||
function Sidebar({tabs}){
|
||||
|
||||
const sideBarRef = useRef()
|
||||
const sideBarExtraRef = useRef()
|
||||
|
||||
const [activeTab, setActiveTab] = useState(-1) // -1 indicates no active tabs
|
||||
const [hoverIndex, setHoverIndex] = useState(-1) // -1 indicates no active tabs
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
const sidebarTabs = useMemo(() => tabs, [tabs])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [sideBarRef, sideBarExtraRef, sidebarOpen])
|
||||
|
||||
const openSidebar = () => {
|
||||
|
||||
sideBarRef.current?.classList.add("tw-w-[400px]")
|
||||
sideBarRef.current?.classList.remove("tw-w-[80px]")
|
||||
|
||||
setSidebarOpen(true)
|
||||
}
|
||||
|
||||
const closeSidebar = () => {
|
||||
sideBarRef.current?.classList.add("tw-w-[80px]")
|
||||
sideBarRef.current?.classList.remove("tw-w-[400px]")
|
||||
setSidebarOpen(false)
|
||||
setActiveTab(-1)
|
||||
setHoverIndex(-1)
|
||||
}
|
||||
|
||||
const hideOnMouseLeave = () => {
|
||||
|
||||
if (activeTab === -1){
|
||||
closeSidebar()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`tw-relative tw-min-w-[80px] tw-duration-[0.3s] tw-transition-all
|
||||
tw-max-w-[400px] tw-flex tw-h-full
|
||||
${sidebarOpen ? "tw-bg-white tw-w-[400px] tw-shadow-lg": "tw-bg-primaryBg tw-w-[80px]"}
|
||||
`} ref={sideBarRef}
|
||||
onMouseLeave={hideOnMouseLeave}
|
||||
>
|
||||
|
||||
|
||||
<div className="tw-w-full tw-max-w-[80px] tw-h-full tw-flex tw-flex-col tw-gap-4 tw-p-3 tw-place-items-center">
|
||||
{
|
||||
sidebarTabs.map((tab, index) => {
|
||||
return (
|
||||
<div className={`${activeTab === index ? "tw-text-blue-400 " : "tw-text-gray-600"} tw-cursor-pointer
|
||||
hover:tw-text-blue-400 tw-flex tw-flex-col tw-gap-2 tw-place-items-center`}
|
||||
key={tab.name}
|
||||
onMouseEnter={() => {
|
||||
openSidebar()
|
||||
setHoverIndex(index)
|
||||
}}
|
||||
onClick={() => {
|
||||
setActiveTab(index)
|
||||
}}
|
||||
>
|
||||
<div className="tw-bg-white tw-shadow-lg tw-p-2 tw-rounded-md">
|
||||
{tab.icon}
|
||||
</div>
|
||||
<span className="tw-text-[12px] ">{tab.name}</span>
|
||||
</div>
|
||||
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="tw-w-full tw-h-full tw-bg-inherit tw-flex tw-flex-col tw-overflow-x-hidden" ref={sideBarExtraRef}>
|
||||
<div className="tw-w-full tw-h-[50px] tw-flex tw-place-content-end tw-p-1">
|
||||
<button className="tw-outline-none tw-bg-transparent tw-border-none tw-text-gray-600 tw-cursor-pointer tw-text-xl"
|
||||
onClick={closeSidebar}
|
||||
>
|
||||
<CloseCircleFilled />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tw-flex tw-w-full tw-h-full tw-max-h-full tw-overflow-y-auto">
|
||||
{(activeTab > -1 || hoverIndex > -1) && tabs[activeTab > -1 ? activeTab : hoverIndex].content}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default Sidebar;
|
||||
100
src/sidebar/widgetsContainer.js
Normal file
100
src/sidebar/widgetsContainer.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
|
||||
import { CloseCircleFilled, SearchOutlined } from "@ant-design/icons"
|
||||
|
||||
import DraggableWidgetCard from "../components/utils/widgetCard"
|
||||
|
||||
import ButtonWidget from "../assets/widgets/button.png"
|
||||
|
||||
import { filterObjectListStartingWith } from "../utils/filter"
|
||||
|
||||
function WidgetsContainer(){
|
||||
|
||||
const widgets = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: "TopLevel",
|
||||
img: ButtonWidget,
|
||||
link: "https://github.com"
|
||||
},
|
||||
{
|
||||
name: "Frame",
|
||||
img: ButtonWidget,
|
||||
link: "https://github.com"
|
||||
},
|
||||
{
|
||||
name: "Button",
|
||||
img: ButtonWidget,
|
||||
link: "https://github.com"
|
||||
},
|
||||
{
|
||||
name: "Input",
|
||||
img: ButtonWidget,
|
||||
link: "https://github.com"
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
|
||||
const [searchValue, setSearchValue] = useState("")
|
||||
const [widgetData, setWidgetData] = useState(widgets)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setWidgetData(widgets)
|
||||
|
||||
}, [widgets])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (searchValue.length > 0){
|
||||
const searchData = filterObjectListStartingWith(widgets, "name", searchValue)
|
||||
setWidgetData(searchData)
|
||||
}else{
|
||||
setWidgetData(widgets)
|
||||
}
|
||||
|
||||
}, [searchValue])
|
||||
|
||||
function onSearch(event){
|
||||
|
||||
setSearchValue(event.target.value)
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tw-w-full tw-p-2 tw-gap-4 tw-flex tw-flex-col">
|
||||
|
||||
<div className="tw-flex tw-gap-2 input tw-place-items-center">
|
||||
<SearchOutlined />
|
||||
<input type="text" placeholder="Search" className="tw-outline-none tw-w-full tw-border-none"
|
||||
id="" onInput={onSearch} value={searchValue}/>
|
||||
<div className="">
|
||||
{
|
||||
searchValue.length > 0 &&
|
||||
<div className="tw-cursor-pointer tw-text-gray-600" onClick={() => setSearchValue("")}>
|
||||
<CloseCircleFilled />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-flex tw-flex-col tw-gap-2 tw-h-full tw-p-1">
|
||||
{
|
||||
widgetData.map((widget, index) => {
|
||||
return (
|
||||
<DraggableWidgetCard key={widget.name}
|
||||
name={widget.name}
|
||||
img={widget.img}
|
||||
url={widget.link}
|
||||
/>
|
||||
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default WidgetsContainer
|
||||
@@ -1,13 +1,25 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
*{
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
|
||||
.input{
|
||||
border: 2px solid #e3e5e8;
|
||||
padding: 2px 8px;
|
||||
min-height: 40px;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:active, .input:focus, .input:focus-within{
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
21
src/utils/filter.js
Normal file
21
src/utils/filter.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* given a list of objects filters out objects starting with specific value for a given key
|
||||
* @param {any[]} list
|
||||
* @param {string} key
|
||||
* @param {string} valueStart
|
||||
* @param {boolean} ignoreCase - default true
|
||||
*/
|
||||
export function filterObjectListStartingWith(list, key, valueStart, ignoreCase = true) {
|
||||
if (ignoreCase)
|
||||
valueStart = valueStart.toLocaleLowerCase()
|
||||
|
||||
return list.filter(obj => {
|
||||
const value = obj[key]
|
||||
|
||||
if (ignoreCase)
|
||||
return value.toLowerCase().startsWith(valueStart)
|
||||
|
||||
else
|
||||
return value.startsWith(valueStart)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primaryBg: "#f6f7f8"
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user