add README, sidebar adjustability
@@ -0,0 +1,138 @@
|
|||||||
|
<img src="logoExcaliDash.png" alt="[Image Description]" width="80" height="88">
|
||||||
|
|
||||||
|
# ExcaliDash
|
||||||
|
|
||||||
|
A beautiful, self hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration.
|
||||||
|

|
||||||
|
|
||||||
|
[Features](#Features)
|
||||||
|
|
||||||
|
[Installation](#Installation)
|
||||||
|
|
||||||
|
[Development](#Development)
|
||||||
|
|
||||||
|
[Credits](#Credits)
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
## Persistent storage for all your drawings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Real time collaboration
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Search your drawings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Drag and drop drawings into collections
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Export/import your drawings and databases for backup
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## Dockerhub (recommended)
|
||||||
|
|
||||||
|
[Install Docker](https://docs.docker.com/desktop/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download docker-compose.prod.yml
|
||||||
|
curl -OL https://raw.githubusercontent.com/ZimengXiong/ExcaliDash/refs/heads/main/docker-compose.prod.yml
|
||||||
|
|
||||||
|
# Pull images
|
||||||
|
docker compose -f docker-compose.prod.yml pull
|
||||||
|
|
||||||
|
# Run container
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker build
|
||||||
|
|
||||||
|
[Install Docker](https://docs.docker.com/desktop/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository (recommended)
|
||||||
|
git clone git@github.com:ZimengXiong/ExcaliDash.git
|
||||||
|
|
||||||
|
# or, clone with HTTPS
|
||||||
|
# git clone https://github.com/ZimengXiong/ExcaliDash.git
|
||||||
|
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Access the frontend at localhost:6767
|
||||||
|
```
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository (recommended)
|
||||||
|
git clone git@github.com:ZimengXiong/ExcaliDash.git
|
||||||
|
|
||||||
|
# or, clone with HTTPS
|
||||||
|
# git clone https://github.com/ZimengXiong/ExcaliDash.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ExcaliDash/frontend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Copy environment file and customize if needed
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ExcaliDash/backend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Copy environment file and customize if needed
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Generate Prisma client and setup database
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma db push
|
||||||
|
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ExcaliDash/
|
||||||
|
├── backend/ # Node.js + Express + Prisma
|
||||||
|
│ ├── src/
|
||||||
|
│ │ └── index.ts # Main server file
|
||||||
|
│ ├── prisma/
|
||||||
|
│ │ ├── schema.prisma # Database schema
|
||||||
|
│ │ └── dev.db # SQLite database
|
||||||
|
│ └── package.json
|
||||||
|
├── frontend/ # React + TypeScript + Vite
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ ├── pages/ # Page components
|
||||||
|
│ │ ├── hooks/ # Custom hooks
|
||||||
|
│ │ └── api/ # API client
|
||||||
|
│ └── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
|
||||||
|
- Example designs from:
|
||||||
|
- https://github.com/Prakash-sa/system-design-ultimatum/tree/main
|
||||||
|
- https://github.com/kitsteam/excalidraw-examples/tree/main
|
||||||
|
- [Excalidraw](https://github.com/ZimengXiong/ExcaliDash)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 9.8 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
import type { Collection } from '../types';
|
import type { Collection } from '../types';
|
||||||
|
|
||||||
@@ -23,10 +23,51 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
onDeleteCollection,
|
onDeleteCollection,
|
||||||
onDrop
|
onDrop
|
||||||
}) => {
|
}) => {
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useState(260);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const startWidthRef = useRef(0);
|
||||||
|
|
||||||
|
// Handle mouse down on resize handle
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsResizing(true);
|
||||||
|
startXRef.current = e.clientX;
|
||||||
|
startWidthRef.current = sidebarWidth;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const diff = e.clientX - startXRef.current;
|
||||||
|
const newWidth = Math.max(200, Math.min(600, startWidthRef.current + diff));
|
||||||
|
setSidebarWidth(newWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsResizing(false);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup event listeners on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', () => {});
|
||||||
|
document.removeEventListener('mouseup', () => {});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full bg-[#F3F4F6] dark:bg-neutral-950 p-4 transition-colors duration-200 overflow-hidden">
|
<div className="h-screen w-full bg-[#F3F4F6] dark:bg-neutral-950 p-4 transition-colors duration-200 overflow-hidden">
|
||||||
<div className="flex gap-4 items-start h-full">
|
<div className="flex gap-4 items-start h-full">
|
||||||
<aside className="flex-shrink-0 w-[260px] h-full bg-white dark:bg-neutral-900 rounded-2xl border-2 border-black dark:border-neutral-700 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] overflow-hidden z-20 transition-colors duration-200">
|
<aside
|
||||||
|
ref={sidebarRef}
|
||||||
|
className="flex-shrink-0 h-full bg-white dark:bg-neutral-900 rounded-2xl border-2 border-black dark:border-neutral-700 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] overflow-hidden z-20 transition-colors duration-200 relative"
|
||||||
|
style={{ width: `${sidebarWidth}px` }}
|
||||||
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collections={collections}
|
collections={collections}
|
||||||
selectedCollectionId={selectedCollectionId}
|
selectedCollectionId={selectedCollectionId}
|
||||||
@@ -36,6 +77,15 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
onDeleteCollection={onDeleteCollection}
|
onDeleteCollection={onDeleteCollection}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Resize Handle */}
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 right-0 w-1.5 h-full cursor-col-resize bg-transparent hover:bg-indigo-400 dark:hover:bg-indigo-500 transition-all duration-150 ${isResizing ? 'bg-indigo-500 dark:bg-indigo-400 w-2' : ''} group`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
title="Drag to resize sidebar"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-y-0 -left-0.5 -right-0.5 bg-transparent hover:bg-indigo-500/10 dark:hover:bg-indigo-400/10 transition-colors duration-150" />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="flex-1 bg-white/40 dark:bg-neutral-900/40 backdrop-blur-sm rounded-2xl border border-white/50 dark:border-neutral-800/50 shadow-sm h-full transition-colors duration-200 overflow-y-auto">
|
<main className="flex-1 bg-white/40 dark:bg-neutral-900/40 backdrop-blur-sm rounded-2xl border border-white/50 dark:border-neutral-800/50 shadow-sm h-full transition-colors duration-200 overflow-y-auto">
|
||||||
<div className="max-w-[1600px] mx-auto p-6 lg:p-8 min-h-full">
|
<div className="max-w-[1600px] mx-auto p-6 lg:p-8 min-h-full">
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
|
|||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group/item px-3">
|
<div className="relative group/item pl-3 pr-2">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<form onSubmit={onEditSubmit} className="py-1">
|
<form onSubmit={onEditSubmit} className="py-1">
|
||||||
<input
|
<input
|
||||||
@@ -97,9 +97,9 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
|
|||||||
<span className={clsx("transition-colors duration-200", isActive || isDragOver ? "text-indigo-900 dark:text-neutral-200" : "text-slate-400 dark:text-neutral-500 group-hover:text-slate-900 dark:group-hover:text-neutral-200")}>
|
<span className={clsx("transition-colors duration-200", isActive || isDragOver ? "text-indigo-900 dark:text-neutral-200" : "text-slate-400 dark:text-neutral-500 group-hover:text-slate-900 dark:group-hover:text-neutral-200")}>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate flex-1 text-left font-bold">{label}</span>
|
<span className="min-w-0 flex-1 text-left font-bold">{label}</span>
|
||||||
{extraAction && (
|
{extraAction && (
|
||||||
<div className="opacity-0 group-hover/item:opacity-100 transition-all duration-200 flex items-center gap-1 translate-x-2 group-hover/item:translate-x-0">
|
<div className="opacity-0 group-hover/item:opacity-100 transition-all duration-200 flex items-center gap-1 flex-shrink-0">
|
||||||
{extraAction}
|
{extraAction}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -166,7 +166,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-[260px] flex flex-col h-full bg-transparent">
|
<div className="w-full flex flex-col h-full bg-transparent">
|
||||||
<div className="p-5 pb-2">
|
<div className="p-5 pb-2">
|
||||||
<h1 className="text-2xl text-slate-900 dark:text-white flex items-center gap-3 tracking-tight" style={{ fontFamily: 'Excalifont' }}>
|
<h1 className="text-2xl text-slate-900 dark:text-white flex items-center gap-3 tracking-tight" style={{ fontFamily: 'Excalifont' }}>
|
||||||
<Logo className="w-10 h-10" />
|
<Logo className="w-10 h-10" />
|
||||||
@@ -182,7 +182,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
<div className="px-6 pb-2 text-[11px] font-bold text-slate-400 dark:text-neutral-500 uppercase tracking-wider">
|
<div className="px-6 pb-2 text-[11px] font-bold text-slate-400 dark:text-neutral-500 uppercase tracking-wider">
|
||||||
Library
|
Library
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3">
|
<div className="pl-3 pr-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelectCollection(undefined)}
|
onClick={() => onSelectCollection(undefined)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -193,7 +193,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<LayoutGrid size={18} className={clsx(selectedCollectionId === undefined ? "text-indigo-900 dark:text-neutral-200" : "text-slate-400 dark:text-neutral-500")} />
|
<LayoutGrid size={18} className={clsx(selectedCollectionId === undefined ? "text-indigo-900 dark:text-neutral-200" : "text-slate-400 dark:text-neutral-500")} />
|
||||||
All Drawings
|
<span className="min-w-0 flex-1 text-left">All Drawings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -252,35 +252,12 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
onEditSubmit={handleEditSubmit}
|
onEditSubmit={handleEditSubmit}
|
||||||
onEditBlur={() => setEditingId(null)}
|
onEditBlur={() => setEditingId(null)}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
extraAction={
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setEditingId(collection.id);
|
|
||||||
setEditName(collection.name);
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-slate-400 dark:text-neutral-500 hover:text-indigo-600 dark:hover:text-neutral-200 hover:bg-indigo-50 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
<Edit2 size={12} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setCollectionToDelete(collection.id);
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-slate-400 dark:text-neutral-500 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-900/30 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
<Trash2 size={12} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="p-4 border-t border-slate-200/50 dark:border-slate-700/50 space-y-2">
|
<div className="px-3 pt-4 pb-4 border-t border-slate-200/50 dark:border-slate-700/50 space-y-2">
|
||||||
<button
|
<button
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -296,14 +273,14 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
navigate('/collections?id=trash');
|
navigate('/collections?id=trash');
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full flex items-center gap-3 px-3 py-2 text-sm font-bold rounded-xl transition-all duration-200 border-2 border-black dark:border-neutral-700 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] mb-4",
|
"w-full flex items-center gap-3 px-3 py-2 text-sm font-bold rounded-xl transition-all duration-200 border-2 border-black dark:border-neutral-700 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)]",
|
||||||
selectedCollectionId === 'trash' || isTrashDragOver
|
selectedCollectionId === 'trash' || isTrashDragOver
|
||||||
? "bg-rose-50 dark:bg-rose-900/30 text-rose-900 dark:text-rose-300 -translate-y-0.5"
|
? "bg-rose-50 dark:bg-rose-900/30 text-rose-900 dark:text-rose-300 -translate-y-0.5"
|
||||||
: "bg-white dark:bg-neutral-900 text-slate-900 dark:text-neutral-200 hover:bg-rose-50 dark:hover:bg-rose-900/30 hover:text-rose-900 dark:hover:text-rose-300 hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] dark:hover:shadow-[4px_4px_0px_0px_rgba(255,255,255,0.2)] hover:-translate-y-0.5"
|
: "bg-white dark:bg-neutral-900 text-slate-900 dark:text-neutral-200 hover:bg-rose-50 dark:hover:bg-rose-900/30 hover:text-rose-900 dark:hover:text-rose-300 hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] dark:hover:shadow-[4px_4px_0px_0px_rgba(255,255,255,0.2)] hover:-translate-y-0.5"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 size={18} />
|
||||||
Trash
|
<span className="min-w-0 flex-1 text-left">Trash</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -316,7 +293,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SettingsIcon size={18} />
|
<SettingsIcon size={18} />
|
||||||
Settings
|
<span className="min-w-0 flex-1 text-left">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 903 KiB |