added canvas resizing

This commit is contained in:
paul
2024-08-05 22:36:05 +05:30
parent 940fe815c6
commit 5594daec82
14 changed files with 1099 additions and 60 deletions

View File

@@ -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>
);
}

View 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

View File

@@ -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
View 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>
)
}

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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}
>

View 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

View File

@@ -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
View 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"
}