New Icon/Logo

This commit is contained in:
Zimeng Xiong
2025-11-22 09:45:20 -08:00
parent 0c843eb37d
commit 05d472189c
11 changed files with 806 additions and 247 deletions
+428
View File
@@ -0,0 +1,428 @@
# ExcaliDash Developer Guide
## What is ExcaliDash?
ExcaliDash is a full-stack dashboard application for managing and organizing [Excalidraw](https://excalidraw.com) drawings. It provides a interface for creating, organizing, and collaborating on diagrams with features like collections, real-time collaboration, file import/export, and bulk operations.
## Project Overview
### Core Features
- **Drawing Management**: Create, edit, and organize Excalidraw drawings
- **Collections**: Organize drawings into folders/categories
- **Real-time Collaboration**: Multiple users can edit drawings simultaneously
- **Import/Export**: Support for .excalidraw and .json file formats
- **Bulk Operations**: Multi-select drawings for delete, duplicate, and move operations
- **Search & Sort**: Find drawings by name with sorting by name, created date, or modified date
- **Trash System**: Soft delete with permanent delete option
- **Drag & Drop**: Intuitive file dragging and drawing reordering
- **Dark/Light Theme**: Automatic theme detection with manual toggle
### Tech Stack
**Frontend:**
- React 18 + TypeScript
- Vite (build tool)
- Tailwind CSS (styling)
- Excalidraw (drawing canvas)
- Socket.io Client (real-time features)
- React Router (navigation)
- Lucide React (icons)
**Backend:**
- Node.js + Express
- TypeScript
- Prisma ORM
- SQLite database
- Socket.io (real-time server)
**Infrastructure:**
- Docker (containerization)
- Docker Compose (multi-container orchestration)
## Project Structure
```
ExcaliDash/
├── README.md # Project overview
├── AGENTS.md # This file - developer guide
├── DOCKER.md # Docker documentation
├── .gitignore # Git ignore rules
├── .dockerignore # Docker ignore rules
├── docker-compose.yml # Production Docker setup
├── docker-compose.prod.yml # Additional production config
├── publish-docker.sh # Docker deployment script
├── backend/ # Node.js/Express backend
│ ├── src/
│ │ ├── index.ts # Main server file
│ │ └── generated/ # Prisma generated client
│ ├── prisma/
│ │ ├── schema.prisma # Database schema
│ │ ├── migrations/ # Database migrations
│ │ └── dev.db # SQLite database (development)
│ ├── package.json # Backend dependencies
│ ├── Dockerfile # Backend container config
│ ├── .env.example # Environment variables template
│ └── docker-entrypoint.sh # Container startup script
└── frontend/ # React frontend application
├── src/
│ ├── components/ # Reusable UI components
│ ├── pages/ # Route components
│ ├── hooks/ # Custom React hooks
│ ├── context/ # React context providers
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ ├── api/ # API client functions
│ └── assets/ # Static assets
├── public/ # Public assets
├── package.json # Frontend dependencies
├── Dockerfile # Frontend container config
├── vite.config.ts # Vite configuration
├── tailwind.config.js # Tailwind CSS configuration
└── nginx.conf # Nginx configuration for production
```
## Getting Started
### Prerequisites
- Node.js 18+ and npm
- Docker and Docker Compose (for containerized development)
- Git
### Development Setup
#### Option 1: Local Development (Recommended)
1. **Install Backend Dependencies**
```bash
cd backend
npm install
```
2. **Install Frontend Dependencies**
```bash
cd ../frontend
npm install
```
3. **Setup Environment Variables**
```bash
cd ../backend
cp .env.example .env
# Edit .env if needed
```
4. **Initialize Database**
```bash
npx prisma generate
npx prisma db push
```
5. **Start Backend Development Server**
```bash
cd backend
npm run dev
# Server runs on http://localhost:8000
```
6. **Start Frontend Development Server** (in a new terminal)
```bash
cd frontend
npm run dev
# Frontend runs on http://localhost:5173
```
### Environment Variables
**Backend (.env):**
```bash
DATABASE_URL="file:./prisma/dev.db"
PORT=8000
NODE_ENV=development
```
**Frontend (.env.example):**
```bash
VITE_API_URL=http://localhost:8000
```
## Making Changes
### Development Workflow
1. **Create a Feature Branch**
```bash
git checkout -b feature/your-feature-name
```
2. **Make Your Changes**
- Follow the existing code style and patterns
- Add TypeScript types for new features
- Update database schema if needed (see Database section)
3. **Test Your Changes**
- Test both locally and with Docker
- Check that existing functionality still works
4. **Commit and Push**
```bash
git add .
git commit -m "feat: add your feature description"
git push origin feature/your-feature-name
```
### Code Style and Standards
- **TypeScript**: Use strict TypeScript typing
- **Component Structure**: Follow the existing component patterns in `frontend/src/components/`
- **API Design**: RESTful endpoints in `backend/src/index.ts`
- **Database**: Use Prisma migrations for schema changes
- **Styling**: Tailwind CSS classes with the existing design system
### File Organization Guidelines
- **Frontend Components**: Keep related components together
- **Custom Hooks**: Place in `frontend/src/hooks/`
- **Utilities**: Place in `frontend/src/utils/`
- **API Routes**: Add to `backend/src/index.ts`
- **Database Models**: Update `backend/prisma/schema.prisma`
## Database
### Schema Overview
The application uses two main models:
**Collection:**
- `id` (String, UUID) - Primary key
- `name` (String) - Collection name
- `drawings` (Relation) - Related drawings
- `createdAt`, `updatedAt` (DateTime) - Timestamps
**Drawing:**
- `id` (String, UUID) - Primary key
- `name` (String) - Drawing name
- `elements` (String, JSON) - Excalidraw elements
- `appState` (String, JSON) - Excalidraw application state
- `files` (String, JSON) - Associated files
- `preview` (String, SVG) - Thumbnail preview
- `version` (Int) - Version number for conflict detection
- `collectionId` (String, nullable) - Foreign key to Collection
- `createdAt`, `updatedAt` (DateTime) - Timestamps
### Making Database Changes
1. **Modify the Schema**
Edit `backend/prisma/schema.prisma`
2. **Create a Migration**
```bash
cd backend
npx prisma migrate dev --name your_migration_name
```
3. **Update TypeScript Types**
```bash
npx prisma generate
```
4. **Test the Changes**
```bash
npx prisma db push # Apply to development database
```
### Database Commands
```bash
# Generate Prisma client
npx prisma generate
# Create and apply migration
npx prisma migrate dev --name migration_name
# Reset database (development only)
npx prisma migrate reset
# View database in Prisma Studio
npx prisma studio
# Deploy migrations to production
npx prisma migrate deploy
```
## API Documentation
### Base URL
- Development: `http://localhost:8000`
- Production: Configured via environment variables
### Endpoints
#### Drawings
- `GET /drawings` - List drawings (supports search and collection filtering)
- `GET /drawings/:id` - Get single drawing
- `POST /drawings` - Create new drawing
- `PUT /drawings/:id` - Update drawing
- `DELETE /drawings/:id` - Delete drawing permanently
- `POST /drawings/:id/duplicate` - Duplicate drawing
#### Collections
- `GET /collections` - List all collections
- `POST /collections` - Create new collection
- `PUT /collections/:id` - Update collection
- `DELETE /collections/:id` - Delete collection (moves drawings to unorganized)
#### System
- `GET /health` - Health check endpoint
### Real-time Events (Socket.io)
#### Client → Server
- `join-room` - Join drawing room for collaboration
- `cursor-move` - Broadcast cursor position
- `element-update` - Broadcast drawing changes
- `user-activity` - Update user active status
#### Server → Client
- `presence-update` - User presence in room
- `cursor-move` - Other user's cursor position
- `element-update` - Other user's drawing changes
### Environment Setup
**Production Environment Variables:**
- `DATABASE_URL` - SQLite database path
- `PORT` - Backend server port
- `NODE_ENV` - Set to "production"
## Troubleshooting
### Common Issues
1. **Database Connection Error**
- Check that the database file exists in `backend/prisma/dev.db`
- Ensure proper permissions on the database file
- Verify DATABASE_URL in .env
2. **Prisma Client Issues**
- Run `npx prisma generate` to regenerate client
- Clear `node_modules` and reinstall dependencies
3. **Port Already in Use**
- Change PORT in backend/.env
- Update frontend API URL accordingly
4. **Docker Build Failures**
- Check Dockerfile syntax
- Ensure all dependencies are listed in package.json
- Verify build context in docker-compose.yml
5. **Frontend Not Loading**
- Check browser console for errors
- Verify API_URL in frontend environment
- Check network connectivity to backend
## Architecture Details
### Frontend Architecture
The frontend follows a component-based architecture with:
- **Pages**: Route-level components (`src/pages/`)
- **Components**: Reusable UI components (`src/components/`)
- **Hooks**: Custom React hooks for state management (`src/hooks/`)
- **Context**: Global state providers (`src/context/`)
- **Utils**: Utility functions (`src/utils/`)
Key patterns:
- State management using React hooks and context
- API calls centralized in `src/api/`
- TypeScript for type safety throughout
- Tailwind CSS for styling with custom design tokens
### Backend Architecture
The backend follows a traditional MVC pattern:
- **Routes**: API endpoints in `src/index.ts`
- **Models**: Prisma schema definitions
- **Services**: Business logic (can be extracted to separate files)
- **Middleware**: CORS, JSON parsing, etc.
Real-time features:
- Socket.io for WebSocket connections
- Room-based collaboration
- Presence tracking
- Cursor position broadcasting
### Data Flow
1. **Drawing Creation**: Frontend → API → Database
2. **Real-time Updates**: Frontend → Socket.io → Other Frontends
3. **Data Persistence**: Regular API calls for saving state
4. **File Management**: Frontend → API → Database (as JSON)
## Contributing
### Pull Request Process
1. **Ensure all tests pass**
2. **Update documentation** if needed
3. **Add commit messages** following conventional commits
4. **Request review** from maintainers
### Commit Message Format
```
type(scope): description
feat(auth): add user authentication
fix(editor): resolve drawing save issue
docs(api): update endpoint documentation
```
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
## Resources
- [Excalidraw Documentation](https://docs.excalidraw.com/)
- [Prisma Documentation](https://www.prisma.io/docs/)
- [React Documentation](https://react.dev/)
- [Socket.io Documentation](https://socket.io/docs/)
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
- [Vite Documentation](https://vitejs.dev/guide/)
_This documentation is maintained alongside the codebase. Please update it when making significant architectural changes._
+1 -114
View File
@@ -37,7 +37,7 @@ docker compose up -d --build
### Access the application:
Open your browser to `http://localhost:6767`
[http://localhost:6767](http://localhost:6767)
## Publishing to Docker Hub
@@ -53,80 +53,6 @@ The `publish-docker.sh` script builds images for multiple platforms (amd64, arm6
./publish-docker.sh v1.0.0
```
**Prerequisites:**
- Docker Buildx enabled
- Logged in to Docker Hub: `docker login`
**Platforms supported:**
- `linux/amd64` (Intel/AMD x86_64)
- `linux/arm64` (Apple Silicon, ARM servers)
The script will:
1. Create a buildx builder if needed
2. Build both frontend and backend images for all platforms
3. Push to `zimengxiong/excalidash-backend` and `zimengxiong/excalidash-frontend`
4. Tag with both the specified version and `latest`
## Management Commands
### View logs:
```bash
# All services
docker compose logs -f
# Specific service
docker compose logs -f backend
docker compose logs -f frontend
```
### Stop services:
```bash
docker compose down
```
### Stop and remove volumes (clean slate):
```bash
docker compose down -v
```
### Restart services:
```bash
docker compose restart
```
### Check service status:
```bash
docker compose ps
```
## Development
For local development outside Docker, use the existing npm scripts:
**Backend:**
```bash
cd backend
npm install
npm run dev
```
**Frontend:**
```bash
cd frontend
npm install
npm run dev
```
## Database
The SQLite database is stored in a Docker volume named `backend-data` which persists data across container restarts. Database migrations run automatically when the backend container starts.
@@ -150,45 +76,6 @@ NODE_ENV=production
VITE_API_URL=/api
```
## Troubleshooting
### Check health status:
```bash
docker-compose ps
```
Both services should show "healthy" status.
### Reset database:
```bash
docker-compose down -v
docker-compose up -d
```
### View detailed backend logs:
```bash
docker-compose logs backend
```
### Rebuild specific service:
```bash
docker-compose up -d --build backend
```
## Production Deployment
For production deployment:
1. Use proper environment variables
2. Configure proper CORS settings in the backend
3. Add HTTPS/TLS termination (e.g., via reverse proxy like Traefik or nginx)
4. Consider using PostgreSQL instead of SQLite for better concurrency
5. Set up proper backup strategy for the `backend-data` volume
## Port Mapping
- **6767** (external) → **80** (frontend nginx) → Routes to backend on internal network
-102
View File
@@ -1,102 +0,0 @@
# Publishing Docker Images
This document explains how to build and publish multi-platform Docker images for ExcaliDash.
## Quick Start
```bash
# Login to Docker Hub (if not already logged in)
docker login
# Build and push images
./publish-docker.sh
```
## Usage
```bash
./publish-docker.sh [VERSION]
```
**Arguments:**
- `VERSION` (optional): The version tag for the images. Defaults to `latest`.
**Examples:**
```bash
# Build and push with 'latest' tag
./publish-docker.sh
# Build and push with version tag
./publish-docker.sh v1.0.0
./publish-docker.sh 2024.11.21
```
## What It Does
1. **Checks Docker Hub authentication** - Ensures you're logged in
2. **Sets up buildx builder** - Creates or uses existing multi-platform builder
3. **Builds backend image** - For linux/amd64 and linux/arm64 platforms
4. **Builds frontend image** - For linux/amd64 and linux/arm64 platforms
5. **Pushes to Docker Hub** - Uploads to `zimengxiong/excalidash-backend` and `zimengxiong/excalidash-frontend`
## Supported Platforms
- **linux/amd64** - Intel/AMD x86_64 processors (most servers, PCs)
- **linux/arm64** - ARM 64-bit processors (Apple Silicon, ARM servers, Raspberry Pi 4+)
## Requirements
- Docker with buildx support (Docker Desktop or Docker Engine 19.03+)
- Docker Hub account credentials
- Internet connection for pushing images
## Troubleshooting
### "buildx" is not a docker command
Update Docker to a newer version that includes buildx, or install the buildx plugin.
### Authentication error
Run `docker login` and enter your Docker Hub credentials.
### Build fails on specific platform
You can modify the script to build for only one platform:
```bash
# In publish-docker.sh, change:
--platform linux/amd64,linux/arm64
# to:
--platform linux/amd64
```
## Using Published Images
After publishing, users can run ExcaliDash using the pre-built images:
```bash
# Using production compose file (no build step needed)
docker compose -f docker-compose.prod.yml up -d
# Or using regular compose file (will pull if not building)
docker compose pull
docker compose up -d
```
## CI/CD Integration
You can integrate this script into your CI/CD pipeline:
```yaml
# Example GitHub Actions workflow
- name: Build and Push Docker Images
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
./publish-docker.sh ${{ github.ref_name }}
```
View File
+66
View File
@@ -0,0 +1,66 @@
<svg width="100%" height="100%" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.stroke-main { stroke: #E2E8F0; }
.fill-teal { fill: #134E4A; } /* teal-900 */
.stroke-teal { stroke: #2DD4BF; } /* teal-400 */
.fill-orange { fill: #7C2D12; } /* orange-900 */
.stroke-orange { stroke: #FB923C; } /* orange-400 */
.fill-purple { fill: #581C87; } /* purple-900 */
.stroke-purple { stroke: #A78BFA; } /* purple-400 */
.stroke-red { stroke: #F87171; } /* red-400 */
.fill-pencil-body { fill: #FACC15; } /* yellow-400 */
.fill-pencil-eraser { fill: #F472B6; } /* pink-400 */
.fill-pencil-collar { fill: #475569; } /* slate-600 */
.fill-pencil-tip { fill: #E2E8F0; }
</style>
<defs>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8"></feGaussianBlur>
<feOffset dx="4" dy="8" result="offsetblur"></feOffset>
<feComponentTransfer>
<feFuncA type="linear" slope="0.2"></feFuncA>
</feComponentTransfer>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<!-- Group: The Dashboard Grid (2x2) -->
<g class="stroke-main" stroke-width="32" stroke-linecap="round" stroke-linejoin="round">
<!-- Top Left: Bar Chart -->
<rect class="fill-teal" x="40" y="40" width="200" height="200" rx="32" stroke-width="24"></rect>
<path class="stroke-teal" d="M100 180 V 140"></path>
<path class="stroke-teal" d="M140 180 V 100"></path>
<path class="stroke-teal" d="M180 180 V 160"></path>
<!-- Top Right: Pie Chart -->
<rect class="fill-orange" x="272" y="40" width="200" height="200" rx="32" stroke-width="24"></rect>
<circle class="stroke-orange" cx="372" cy="140" r="50" stroke-width="24"></circle>
<!-- Slice -->
<path class="stroke-orange" d="M372 140 L 405 105" stroke-width="12"></path>
<!-- Bottom Left: List/Text -->
<rect class="fill-purple" x="40" y="272" width="200" height="200" rx="32" stroke-width="24"></rect>
<line class="stroke-purple" x1="80" y1="332" x2="200" y2="332"></line>
<line class="stroke-purple" x1="80" y1="372" x2="160" y2="372"></line>
<line class="stroke-purple" x1="80" y1="412" x2="180" y2="412"></line>
<!-- Bottom Right: The "Creation" Zone -->
<!-- Dashed placeholder for the item being created -->
<rect class="stroke-red" x="272" y="272" width="200" height="200" rx="32" fill="none"
stroke-width="24" stroke-dasharray="30 30"></rect>
</g>
<!-- The Pencil (Floating over the bottom right) -->
<g transform="translate(380, 380) rotate(-45)" filter="url(#softShadow)">
<!-- Body -->
<path class="fill-pencil-body stroke-main" d="M-25 -100 L-25 80 L0 120 L25 80 L25 -100 Z"
stroke-width="24" stroke-linejoin="round"></path>
<!-- Eraser -->
<path class="fill-pencil-eraser stroke-main"
d="M-25 -100 L-25 -130 C-25 -150, 25 -150, 25 -130 L25 -100 Z" stroke-width="24"
stroke-linejoin="round"></path>
<!-- Collar -->
<rect class="fill-pencil-collar stroke-main" x="-25" y="-100" width="50" height="30"
stroke-width="24" stroke-linejoin="round"></rect>
<!-- Tip Lead -->
<path class="fill-pencil-tip" d="M-8 108 L0 120 L8 108 Z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+66
View File
@@ -0,0 +1,66 @@
<svg width="100%" height="100%" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.stroke-main { stroke: #2D3748; }
.fill-teal { fill: #E6FFFA; }
.stroke-teal { stroke: #38B2AC; }
.fill-orange { fill: #FFFAF0; }
.stroke-orange { stroke: #DD6B20; }
.fill-purple { fill: #FAF5FF; }
.stroke-purple { stroke: #805AD5; }
.stroke-red { stroke: #E53E3E; }
.fill-pencil-body { fill: #F6E05E; }
.fill-pencil-eraser { fill: #F687B3; }
.fill-pencil-collar { fill: #CBD5E0; }
.fill-pencil-tip { fill: #2D3748; }
</style>
<defs>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8"></feGaussianBlur>
<feOffset dx="4" dy="8" result="offsetblur"></feOffset>
<feComponentTransfer>
<feFuncA type="linear" slope="0.2"></feFuncA>
</feComponentTransfer>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<!-- Group: The Dashboard Grid (2x2) -->
<g class="stroke-main" stroke-width="32" stroke-linecap="round" stroke-linejoin="round">
<!-- Top Left: Bar Chart -->
<rect class="fill-teal" x="40" y="40" width="200" height="200" rx="32" stroke-width="24"></rect>
<path class="stroke-teal" d="M100 180 V 140"></path>
<path class="stroke-teal" d="M140 180 V 100"></path>
<path class="stroke-teal" d="M180 180 V 160"></path>
<!-- Top Right: Pie Chart -->
<rect class="fill-orange" x="272" y="40" width="200" height="200" rx="32" stroke-width="24"></rect>
<circle class="stroke-orange" cx="372" cy="140" r="50" stroke-width="24"></circle>
<!-- Slice -->
<path class="stroke-orange" d="M372 140 L 405 105" stroke-width="12"></path>
<!-- Bottom Left: List/Text -->
<rect class="fill-purple" x="40" y="272" width="200" height="200" rx="32" stroke-width="24"></rect>
<line class="stroke-purple" x1="80" y1="332" x2="200" y2="332"></line>
<line class="stroke-purple" x1="80" y1="372" x2="160" y2="372"></line>
<line class="stroke-purple" x1="80" y1="412" x2="180" y2="412"></line>
<!-- Bottom Right: The "Creation" Zone -->
<!-- Dashed placeholder for the item being created -->
<rect class="stroke-red" x="272" y="272" width="200" height="200" rx="32" fill="none"
stroke-width="24" stroke-dasharray="30 30"></rect>
</g>
<!-- The Pencil (Floating over the bottom right) -->
<g transform="translate(380, 380) rotate(-45)" filter="url(#softShadow)">
<!-- Body -->
<path class="fill-pencil-body stroke-main" d="M-25 -100 L-25 80 L0 120 L25 80 L25 -100 Z"
stroke-width="24" stroke-linejoin="round"></path>
<!-- Eraser -->
<path class="fill-pencil-eraser stroke-main"
d="M-25 -100 L-25 -130 C-25 -150, 25 -150, 25 -130 L25 -100 Z" stroke-width="24"
stroke-linejoin="round"></path>
<!-- Collar -->
<rect class="fill-pencil-collar stroke-main" x="-25" y="-100" width="50" height="30"
stroke-width="24" stroke-linejoin="round"></rect>
<!-- Tip Lead -->
<path class="fill-pencil-tip" d="M-8 108 L0 120 L8 108 Z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+75 -14
View File
@@ -1,20 +1,81 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="100%" height="100%" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.shadow { fill: black; }
.box { fill: #4f46e5; stroke: black; }
.stroke-main { stroke: #2D3748; }
.fill-teal { fill: #E6FFFA; }
.stroke-teal { stroke: #38B2AC; }
.fill-orange { fill: #FFFAF0; }
.stroke-orange { stroke: #DD6B20; }
.fill-purple { fill: #FAF5FF; }
.stroke-purple { stroke: #805AD5; }
.stroke-red { stroke: #E53E3E; }
.fill-pencil-body { fill: #F6E05E; }
.fill-pencil-eraser { fill: #F687B3; }
.fill-pencil-collar { fill: #CBD5E0; }
.fill-pencil-tip { fill: #2D3748; }
@media (prefers-color-scheme: dark) {
.shadow { fill: rgba(255, 255, 255, 0.2); }
.box { fill: #262626; stroke: #404040; }
.stroke-main { stroke: #E2E8F0; }
.fill-teal { fill: #134E4A; } /* teal-900 */
.stroke-teal { stroke: #2DD4BF; } /* teal-400 */
.fill-orange { fill: #7C2D12; } /* orange-900 */
.stroke-orange { stroke: #FB923C; } /* orange-400 */
.fill-purple { fill: #581C87; } /* purple-900 */
.stroke-purple { stroke: #A78BFA; } /* purple-400 */
.stroke-red { stroke: #F87171; } /* red-400 */
.fill-pencil-body { fill: #FACC15; } /* yellow-400 */
.fill-pencil-eraser { fill: #F472B6; } /* pink-400 */
.fill-pencil-collar { fill: #475569; } /* slate-600 */
.fill-pencil-tip { fill: #E2E8F0; }
}
</style>
<rect class="shadow" x="4" y="4" width="26" height="26" rx="6"/>
<rect class="box" x="2" y="2" width="26" height="26" rx="6" stroke-width="2"/>
<g transform="translate(7, 7)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>
<defs>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8"></feGaussianBlur>
<feOffset dx="4" dy="8" result="offsetblur"></feOffset>
<feComponentTransfer>
<feFuncA type="linear" slope="0.2"></feFuncA>
</feComponentTransfer>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<!-- Group: The Dashboard Grid (2x2) -->
<g class="stroke-main" stroke-width="32" stroke-linecap="round" stroke-linejoin="round">
<!-- Top Left: Bar Chart -->
<rect class="fill-teal" x="40" y="40" width="200" height="200" rx="32" stroke-width="24"></rect>
<path class="stroke-teal" d="M100 180 V 140"></path>
<path class="stroke-teal" d="M140 180 V 100"></path>
<path class="stroke-teal" d="M180 180 V 160"></path>
<!-- Top Right: Pie Chart -->
<rect class="fill-orange" x="272" y="40" width="200" height="200" rx="32" stroke-width="24"></rect>
<circle class="stroke-orange" cx="372" cy="140" r="50" stroke-width="24"></circle>
<!-- Slice -->
<path class="stroke-orange" d="M372 140 L 405 105" stroke-width="12"></path>
<!-- Bottom Left: List/Text -->
<rect class="fill-purple" x="40" y="272" width="200" height="200" rx="32" stroke-width="24"></rect>
<line class="stroke-purple" x1="80" y1="332" x2="200" y2="332"></line>
<line class="stroke-purple" x1="80" y1="372" x2="160" y2="372"></line>
<line class="stroke-purple" x1="80" y1="412" x2="180" y2="412"></line>
<!-- Bottom Right: The "Creation" Zone -->
<!-- Dashed placeholder for the item being created -->
<rect class="stroke-red" x="272" y="272" width="200" height="200" rx="32" fill="none"
stroke-width="24" stroke-dasharray="30 30"></rect>
</g>
<!-- The Pencil (Floating over the bottom right) -->
<g transform="translate(380, 380) rotate(-45)" filter="url(#softShadow)">
<!-- Body -->
<path class="fill-pencil-body stroke-main" d="M-25 -100 L-25 80 L0 120 L25 80 L25 -100 Z"
stroke-width="24" stroke-linejoin="round"></path>
<!-- Eraser -->
<path class="fill-pencil-eraser stroke-main"
d="M-25 -100 L-25 -130 C-25 -150, 25 -150, 25 -130 L25 -100 Z" stroke-width="24"
stroke-linejoin="round"></path>
<!-- Collar -->
<rect class="fill-pencil-collar stroke-main" x="-25" y="-100" width="50" height="30"
stroke-width="24" stroke-linejoin="round"></rect>
<!-- Tip Lead -->
<path class="fill-pencil-tip" d="M-8 108 L0 120 L8 108 Z"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 4.1 KiB

+155
View File
@@ -0,0 +1,155 @@
import React from 'react';
export const Logo: React.FC<{ className?: string }> = ({ className }) => {
return (
<svg
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<defs>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8"></feGaussianBlur>
<feOffset dx="4" dy="8" result="offsetblur"></feOffset>
<feComponentTransfer>
<feFuncA type="linear" slope="0.2"></feFuncA>
</feComponentTransfer>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
{/* Group: The Dashboard Grid (2x2) */}
<g
strokeWidth="32"
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-[#2D3748] dark:stroke-[#E2E8F0]"
>
{/* Top Left: Bar Chart */}
<rect
x="40"
y="40"
width="200"
height="200"
rx="32"
strokeWidth="24"
className="fill-[#E6FFFA] dark:fill-[#134E4A]"
></rect>
<path
d="M100 180 V 140"
className="stroke-[#38B2AC] dark:stroke-[#2DD4BF]"
></path>
<path
d="M140 180 V 100"
className="stroke-[#38B2AC] dark:stroke-[#2DD4BF]"
></path>
<path
d="M180 180 V 160"
className="stroke-[#38B2AC] dark:stroke-[#2DD4BF]"
></path>
{/* Top Right: Pie Chart */}
<rect
x="272"
y="40"
width="200"
height="200"
rx="32"
strokeWidth="24"
className="fill-[#FFFAF0] dark:fill-[#7C2D12]"
></rect>
<circle
cx="372"
cy="140"
r="50"
strokeWidth="24"
className="stroke-[#DD6B20] dark:stroke-[#FB923C]"
></circle>
{/* Slice */}
<path
d="M372 140 L 405 105"
strokeWidth="12"
className="stroke-[#DD6B20] dark:stroke-[#FB923C]"
></path>
{/* Bottom Left: List/Text */}
<rect
x="40"
y="272"
width="200"
height="200"
rx="32"
strokeWidth="24"
className="fill-[#FAF5FF] dark:fill-[#581C87]"
></rect>
<line
x1="80"
y1="332"
x2="200"
y2="332"
className="stroke-[#805AD5] dark:stroke-[#A78BFA]"
></line>
<line
x1="80"
y1="372"
x2="160"
y2="372"
className="stroke-[#805AD5] dark:stroke-[#A78BFA]"
></line>
<line
x1="80"
y1="412"
x2="180"
y2="412"
className="stroke-[#805AD5] dark:stroke-[#A78BFA]"
></line>
{/* Bottom Right: The "Creation" Zone */}
{/* Dashed placeholder for the item being created */}
<rect
x="272"
y="272"
width="200"
height="200"
rx="32"
fill="none"
strokeWidth="24"
strokeDasharray="30 30"
className="stroke-[#E53E3E] dark:stroke-[#F87171]"
></rect>
</g>
{/* The Pencil (Floating over the bottom right) */}
<g transform="translate(380, 380) rotate(-45)" filter="url(#softShadow)">
{/* Body */}
<path
d="M-25 -100 L-25 80 L0 120 L25 80 L25 -100 Z"
strokeWidth="24"
strokeLinejoin="round"
className="fill-[#F6E05E] dark:fill-[#FACC15] stroke-[#2D3748] dark:stroke-[#E2E8F0]"
></path>
{/* Eraser */}
<path
d="M-25 -100 L-25 -130 C-25 -150, 25 -150, 25 -130 L25 -100 Z"
strokeWidth="24"
strokeLinejoin="round"
className="fill-[#F687B3] dark:fill-[#F472B6] stroke-[#2D3748] dark:stroke-[#E2E8F0]"
></path>
{/* Collar */}
<rect
x="-25"
y="-100"
width="50"
height="30"
strokeWidth="24"
strokeLinejoin="round"
className="fill-[#CBD5E0] dark:fill-[#475569] stroke-[#2D3748] dark:stroke-[#E2E8F0]"
></rect>
{/* Tip Lead */}
<path
d="M-8 108 L0 120 L8 108 Z"
className="fill-[#2D3748] dark:fill-[#E2E8F0]"
></path>
</g>
</svg>
);
};
+2 -4
View File
@@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { LayoutGrid, Folder, Plus, Trash2, Edit2, Archive, FolderOpen, Settings as SettingsIcon } from 'lucide-react';
import type { Collection } from '../types';
import clsx from 'clsx';
import { ConfirmModal } from './ConfirmModal';
import { Logo } from './Logo';
interface SidebarProps {
collections: Collection[];
@@ -169,9 +169,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
<div className="w-[260px] flex flex-col h-full bg-transparent">
<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' }}>
<div className="w-8 h-8 bg-indigo-600 dark:bg-neutral-800 rounded-lg flex items-center justify-center text-white 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)]">
<LayoutGrid size={18} strokeWidth={2.5} />
</div>
<Logo className="w-10 h-10" />
<span className="mt-1">ExcaliDash</span>
</h1>
</div>
+7
View File
@@ -18,6 +18,13 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
useEffect(() => {
console.log('Theme changed to:', theme);
localStorage.setItem('theme', theme);
// Update favicon
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (link) {
link.href = theme === 'dark' ? '/favicon-dark.svg' : '/favicon-light.svg';
}
if (theme === 'dark') {
document.documentElement.classList.add('dark');
console.log('Added dark class, classList:', document.documentElement.classList.toString());
+1 -8
View File
@@ -62,16 +62,9 @@ docker buildx build \
echo -e "${GREEN}✓ Frontend image pushed successfully${NC}"
# Summary
echo ""
echo -e "${GREEN}===========================================${NC}"
echo -e "${GREEN}✓ All images published successfully!${NC}"
echo -e "${GREEN}===========================================${NC}"
echo ""
echo -e "Published images:"
echo -e " Backend: ${YELLOW}$DOCKER_USERNAME/$IMAGE_NAME-backend:$VERSION${NC}"
echo -e " Frontend: ${YELLOW}$DOCKER_USERNAME/$IMAGE_NAME-frontend:$VERSION${NC}"
echo ""
echo -e "To use these images:"
echo -e " ${YELLOW}docker compose -f docker-compose.prod.yml up -d${NC}"
echo ""