added canvas resizing
This commit is contained in:
17
src/App.js
17
src/App.js
@@ -1,11 +1,17 @@
|
||||
import Sidebar from './sidebar/sidebar';
|
||||
import { useState } from 'react'
|
||||
|
||||
import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/icons";
|
||||
import WidgetsContainer from './sidebar/widgetsContainer';
|
||||
import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/icons"
|
||||
|
||||
import Sidebar from './sidebar/sidebar'
|
||||
import WidgetsContainer from './sidebar/widgetsContainer'
|
||||
import UploadsContainer from './sidebar/uploadsContainer'
|
||||
import Canvas from './canvas/main'
|
||||
|
||||
function App() {
|
||||
|
||||
const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
|
||||
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "Widgets",
|
||||
@@ -20,7 +26,8 @@ function App() {
|
||||
{
|
||||
name: "Uploads",
|
||||
icon: <CloudUploadOutlined />,
|
||||
content: <></>
|
||||
content: <UploadsContainer assets={uploadedAssets}
|
||||
onAssetUploadChange={(assets) => setUploadedAssets(assets)}/>
|
||||
}
|
||||
]
|
||||
|
||||
@@ -28,7 +35,7 @@ function App() {
|
||||
<div className="tw-w-full tw-h-[100vh] tw-flex tw-bg-primaryBg">
|
||||
|
||||
<Sidebar tabs={tabs}/>
|
||||
|
||||
<Canvas />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
62
src/canvas/fabricCanvas.js
Normal file
62
src/canvas/fabricCanvas.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import * as fabric from 'fabric'
|
||||
|
||||
|
||||
function FabricJSCanvas({ canvasOptions, className = '', onCanvasContextUpdate }) {
|
||||
|
||||
const canvasRef = useRef(null)
|
||||
|
||||
const fabricCanvasRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const options = {}
|
||||
let canvas = null
|
||||
if (canvasRef.current) {
|
||||
canvas = new fabric.Canvas(canvasRef.current, options)
|
||||
const parent = canvasRef.current.parentNode.parentNode
|
||||
canvas.setDimensions({ width: parent.clientWidth, height: parent.clientHeight })
|
||||
canvas.calcOffset()
|
||||
|
||||
fabricCanvasRef.current = canvas
|
||||
canvasRef.current.parentNode.style.width = "100%"
|
||||
canvasRef.current.parentNode.style.height = "100%"
|
||||
|
||||
console.log("Parent: ", canvasRef.current.parentNode)
|
||||
|
||||
window.addEventListener("resize", updateCanvasDimensions)
|
||||
|
||||
// make the fabric.Canvas instance available to your app
|
||||
if (onCanvasContextUpdate)
|
||||
onCanvasContextUpdate(canvas)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateCanvasDimensions)
|
||||
|
||||
if (onCanvasContextUpdate)
|
||||
onCanvasContextUpdate(null)
|
||||
|
||||
canvas.dispose()
|
||||
}
|
||||
}, [canvasRef])
|
||||
|
||||
|
||||
const updateCanvasDimensions = useCallback(() => {
|
||||
if (!canvasRef.current || !fabricCanvasRef.current)
|
||||
return
|
||||
|
||||
const parent = canvasRef.current.parentNode.parentNode
|
||||
|
||||
fabricCanvasRef.current.setDimensions({ width: parent.clientWidth, height: parent.clientHeight })
|
||||
fabricCanvasRef.current.renderAll()
|
||||
|
||||
}, [fabricCanvasRef, canvasRef])
|
||||
|
||||
|
||||
|
||||
return <canvas className={className} ref={canvasRef} id='we'/>
|
||||
}
|
||||
|
||||
|
||||
export default FabricJSCanvas
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import FabricJSCanvas from "./fabricCanvas"
|
||||
|
||||
|
||||
function Canvas(){
|
||||
|
||||
const fabricCanvasRef = useRef()
|
||||
|
||||
const updateCanvasDimensions = useCallback((event) => {
|
||||
console.log("Updating: ", fabricCanvasRef)
|
||||
if (!fabricCanvasRef.current)
|
||||
return
|
||||
|
||||
const parent = event.target
|
||||
|
||||
fabricCanvasRef.current.setDimensions({ width: parent.clientWidth, height: parent.clientHeight })
|
||||
fabricCanvasRef.current.renderAll()
|
||||
|
||||
}, [fabricCanvasRef])
|
||||
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-w-full tw-h-full tw-max-h-[100vh] tw-overflow-auto"
|
||||
|
||||
>
|
||||
<FabricJSCanvas className="tw-bg-red-200"
|
||||
onCanvasContextUpdate={(canvas) => fabricCanvasRef.current = canvas}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Canvas
|
||||
113
src/components/cards.js
Normal file
113
src/components/cards.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import Draggable from "./utils/draggable"
|
||||
|
||||
import { FileImageOutlined, GithubOutlined, GitlabOutlined, LinkOutlined,
|
||||
AudioOutlined, VideoCameraOutlined,
|
||||
FileTextOutlined} from "@ant-design/icons"
|
||||
|
||||
|
||||
export 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 function DraggableAssetCard({file}){
|
||||
|
||||
const videoRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
function playOnMouseEnter(){
|
||||
videoRef.current.play()
|
||||
}
|
||||
|
||||
function pauseOnMouseEnter(){
|
||||
videoRef.current.pause()
|
||||
videoRef.current.currentTime = 0
|
||||
}
|
||||
|
||||
if (videoRef.current){
|
||||
videoRef.current.addEventListener("mouseenter", playOnMouseEnter)
|
||||
videoRef.current.addEventListener("mouseleave", pauseOnMouseEnter)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (videoRef.current){
|
||||
videoRef.current.removeEventListener("mouseenter", playOnMouseEnter)
|
||||
videoRef.current.removeEventListener("mouseleave", pauseOnMouseEnter)
|
||||
}
|
||||
}
|
||||
|
||||
}, [videoRef])
|
||||
|
||||
|
||||
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-flex tw-place-content-center tw-p-1 tw-text-3xl tw-overflow-hidden">
|
||||
{ file.fileType === "image" &&
|
||||
<img src={file.previewUrl} alt={file.name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
|
||||
}
|
||||
|
||||
{
|
||||
file.fileType === "video" &&
|
||||
<video className="tw-w-full tw-object-contain" ref={videoRef} muted>
|
||||
<source src={file.previewUrl} type={`${file.type || "video/mp4"}`} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
}
|
||||
|
||||
{
|
||||
file.fileType === "audio" &&
|
||||
<AudioOutlined />
|
||||
}
|
||||
{
|
||||
file.fileType === "others" &&
|
||||
<FileTextOutlined />
|
||||
}
|
||||
|
||||
</div>
|
||||
<span className="tw-text-lg">{file.name}</span>
|
||||
</div>
|
||||
</Draggable>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
29
src/redux/assetSlice.js
Normal file
29
src/redux/assetSlice.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
/**
|
||||
* contains global storage for user
|
||||
*/
|
||||
|
||||
|
||||
const initialState = {
|
||||
files: []
|
||||
}
|
||||
|
||||
export const assetSlice = createSlice({
|
||||
|
||||
name: 'assets',
|
||||
initialState,
|
||||
|
||||
reducers:{
|
||||
|
||||
update: (state, action) => {
|
||||
state.files = action.payload.files
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
export const { update } = assetSlice.actions
|
||||
|
||||
export default assetSlice.reducer
|
||||
@@ -1,9 +1,11 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
|
||||
import userReducer from "./assetSlice"
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
assets: userReducer,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default store;
|
||||
export default store
|
||||
@@ -44,8 +44,8 @@ function Sidebar({tabs}){
|
||||
|
||||
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]"}
|
||||
tw-max-w-[400px] tw-flex tw-h-full tw-z-10
|
||||
${sidebarOpen ? "tw-bg-white tw-min-w-[400px] tw-w-[400px] tw-shadow-lg": "tw-bg-primaryBg tw-w-[80px]"}
|
||||
`} ref={sideBarRef}
|
||||
onMouseLeave={hideOnMouseLeave}
|
||||
>
|
||||
|
||||
151
src/sidebar/uploadsContainer.js
Normal file
151
src/sidebar/uploadsContainer.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
// import { useDispatch, useSelector } from "react-redux"
|
||||
|
||||
import { Upload, message } from "antd"
|
||||
|
||||
import { CloseCircleFilled, InboxOutlined, SearchOutlined } from "@ant-design/icons"
|
||||
|
||||
import { DraggableAssetCard } from "../components/cards.js"
|
||||
import { filterObjectListStartingWith } from "../utils/filter"
|
||||
import { getFileType } from "../utils/file.js"
|
||||
// import { update } from "../redux/assetSlice.js"
|
||||
|
||||
|
||||
|
||||
const { Dragger } = Upload
|
||||
const fileTypes = ["JPG", "PNG", "GIF"]
|
||||
|
||||
const props = {
|
||||
name: 'file',
|
||||
multiple: true,
|
||||
showUploadList: false,
|
||||
customRequest(options){
|
||||
const { onSuccess, onError, file, onProgress } = options
|
||||
onSuccess("Ok")
|
||||
},
|
||||
// onDrop(e) {
|
||||
// console.log('Dropped files', e.dataTransfer.files)
|
||||
// },
|
||||
};
|
||||
|
||||
/**
|
||||
* DND for file uploads
|
||||
*
|
||||
*/
|
||||
function UploadsContainer({assets, onAssetUploadChange}) {
|
||||
|
||||
// const dispatch = useDispatch()
|
||||
// const selector = useSelector(state => state.assets)
|
||||
|
||||
const fileUploadRef = useRef()
|
||||
const [dragEnter, setDragEnter] = useState(false)
|
||||
|
||||
const [searchValue, setSearchValue] = useState("")
|
||||
const [uploadData, setUploadData] = useState(assets) // contains all the uploaded data
|
||||
const [uploadResults, setUploadResults] = useState(assets) // sets search results
|
||||
|
||||
useEffect(() => {
|
||||
setUploadResults(uploadData)
|
||||
}, [uploadData])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (searchValue.length > 0) {
|
||||
const searchData = filterObjectListStartingWith(uploadData, "name", searchValue)
|
||||
setUploadResults(searchData)
|
||||
} else {
|
||||
setUploadResults(uploadData)
|
||||
}
|
||||
|
||||
}, [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"
|
||||
onDragEnter={() => { setDragEnter(true) }}
|
||||
onDragLeave={(e) => {
|
||||
// Ensure the drag leave is happening on the container and not on a child element
|
||||
if (e.currentTarget.contains(e.relatedTarget)) {
|
||||
return
|
||||
}
|
||||
setDragEnter(false)
|
||||
}}
|
||||
>
|
||||
|
||||
<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-relative tw-flex-col tw-gap-2 tw-h-full tw-p-1 tw-pb-4">
|
||||
<Dragger className={`${dragEnter && "!tw-h-[80vh] !tw-opacity-100 !tw-bg-[#fafafa] tw-absolute tw-top-0 tw-z-10"} tw-w-full !tw-min-w-[250px]`}
|
||||
{...props}
|
||||
ref={fileUploadRef}
|
||||
onChange = {
|
||||
(info) => {
|
||||
const { status } = info.file
|
||||
|
||||
if (status === 'done') {
|
||||
// console.log("file: ", info)
|
||||
let previewUrl = ""
|
||||
|
||||
const fileType = getFileType(info.file)
|
||||
|
||||
if (fileType === "image" || fileType === "video"){
|
||||
previewUrl = URL.createObjectURL(info.file.originFileObj)
|
||||
}
|
||||
|
||||
const newFileData = {
|
||||
...info.file,
|
||||
previewUrl,
|
||||
fileType
|
||||
}
|
||||
// dispatch(update([newFileData, ...uploadData]))
|
||||
setUploadData(prev => [newFileData, ...prev])
|
||||
onAssetUploadChange([newFileData, ...uploadData])
|
||||
setDragEnter(false)
|
||||
|
||||
message.success(`${info.file.name} file uploaded successfully.`)
|
||||
} else if (status === 'error') {
|
||||
message.error(`${info.file.name} file upload failed.`)
|
||||
setDragEnter(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">Click or drag file to this area to upload</p>
|
||||
</Dragger>
|
||||
{
|
||||
uploadResults.map((file, index) => {
|
||||
return (
|
||||
<DraggableAssetCard key={file.uid}
|
||||
file={file}
|
||||
/>
|
||||
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default UploadsContainer
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"
|
||||
|
||||
import { CloseCircleFilled, SearchOutlined } from "@ant-design/icons"
|
||||
|
||||
import DraggableWidgetCard from "../components/utils/widgetCard"
|
||||
import {DraggableWidgetCard} from "../components/cards"
|
||||
|
||||
import ButtonWidget from "../assets/widgets/button.png"
|
||||
|
||||
|
||||
17
src/utils/file.js
Normal file
17
src/utils/file.js
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
const ImageFileExtensions = ["png", "gif", "jpg", "jpeg", "webp"]
|
||||
const VideoFileExtensions = ["mp4", "webm", "m4v", "webm"]
|
||||
|
||||
export function getFileType(file){
|
||||
|
||||
if (file.type?.startsWith("image") || ImageFileExtensions.includes(file.name.split(".").at(-1))){
|
||||
return "image"
|
||||
}else if (file.type?.startsWith("video") || VideoFileExtensions.includes(file.name.split(".").at(-1))){
|
||||
return "video"
|
||||
}else if(file.type?.startsWith("audio")){
|
||||
return "audio"
|
||||
}
|
||||
|
||||
return "others"
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user