diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..6dcefa4 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,44 @@ +# Git +.git +.gitignore + +# Environment files (don't include in image, use Fly.io secrets) +.env +.env.local +.env.*.local + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# Test files +*_test.go +testdata + +# Documentation +*.md +README.md + +# Scripts (migration files should be run separately) +scripts + +# Temporary files +*.log +tmp +temp + +# Build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +server +main + +# OS files +.DS_Store +Thumbs.db diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..81568af --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,52 @@ +# Build stage +FROM golang:1.25.3-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata + +# Set working directory +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +# CGO_ENABLED=0 for static binary, GOOS=linux for Linux compatibility +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o server ./cmd/server/main.go + +# Runtime stage +FROM alpine:latest + +# Install CA certificates for HTTPS requests (OAuth) +RUN apk --no-cache add ca-certificates tzdata + +# Create non-root user +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /app/server . + +# Change ownership to non-root user +RUN chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Expose port (Fly.io uses PORT env variable) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# Run the application +CMD ["./server"] diff --git a/backend/fly.toml b/backend/fly.toml new file mode 100644 index 0000000..1e1125c --- /dev/null +++ b/backend/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for docnest-backend-mingda on 2026-01-11T23:46:03-08:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'docnest-backend-mingda' +primary_region = 'sjc' + +[build] + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpus = 1 + memory_mb = 1024 diff --git a/backend/internal/models/version.go b/backend/internal/models/version.go new file mode 100644 index 0000000..4753ff7 --- /dev/null +++ b/backend/internal/models/version.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +// DocumentVersion represents a snapshot of a document at a point in time +type DocumentVersion struct { + ID uuid.UUID `json:"id"` + DocumentID uuid.UUID `json:"document_id"` + YjsSnapshot []byte `json:"-"` // Omit from JSON (binary) + TextPreview *string `json:"text_preview"` // Full plain text + VersionNumber int `json:"version_number"` + CreatedBy *uuid.UUID `json:"created_by"` + VersionLabel *string `json:"version_label"` + IsAutoGenerated bool `json:"is_auto_generated"` + CreatedAt time.Time `json:"created_at"` +} + +// DocumentVersionWithAuthor includes author information +type DocumentVersionWithAuthor struct { + DocumentVersion + Author *User `json:"author,omitempty"` // Nullable if user deleted +} + +// CreateVersionRequest is the API request for manual snapshots +type CreateVersionRequest struct { + VersionLabel *string `json:"version_label"` // Optional user label + TextPreview *string `json:"text_preview"` // Frontend-generated full text + // YjsSnapshot sent as multipart file, not JSON +} + +// RestoreVersionRequest specifies which version to restore +type RestoreVersionRequest struct { + VersionID uuid.UUID `json:"version_id" binding:"required"` +} + +// VersionListResponse for listing versions +type VersionListResponse struct { + Versions []DocumentVersionWithAuthor `json:"versions"` + Total int `json:"total"` +} diff --git a/backend/internal/store/postgres.go b/backend/internal/store/postgres.go index 8da64e8..01974e6 100644 --- a/backend/internal/store/postgres.go +++ b/backend/internal/store/postgres.go @@ -47,6 +47,13 @@ type Store interface { GetUserPermission(ctx context.Context, documentID, userID uuid.UUID) (string, error) GetShareLinkPermission(ctx context.Context, documentID uuid.UUID) (string, error) + // Version operations + CreateDocumentVersion(ctx context.Context, documentID, userID uuid.UUID, snapshot []byte, textPreview *string, label *string, isAuto bool) (*models.DocumentVersion, error) + ListDocumentVersions(ctx context.Context, documentID uuid.UUID, limit int, offset int) ([]models.DocumentVersionWithAuthor, int, error) + GetDocumentVersion(ctx context.Context, versionID uuid.UUID) (*models.DocumentVersion, error) + GetLatestDocumentVersion(ctx context.Context, documentID uuid.UUID) (*models.DocumentVersion, error) + + Close() error } diff --git a/backend/internal/store/version.go b/backend/internal/store/version.go new file mode 100644 index 0000000..8f446ec --- /dev/null +++ b/backend/internal/store/version.go @@ -0,0 +1,194 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + + "github.com/M1ngdaXie/realtime-collab/internal/models" + "github.com/google/uuid" +) + +// CreateDocumentVersion creates a new version snapshot +func (s *PostgresStore) CreateDocumentVersion( + ctx context.Context, + documentID, userID uuid.UUID, + snapshot []byte, + textPreview *string, + label *string, + isAuto bool, +) (*models.DocumentVersion, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + // Get next version number + var versionNumber int + err = tx.QueryRowContext(ctx, + `SELECT get_next_version_number($1)`, + documentID, + ).Scan(&versionNumber) + if err != nil { + return nil, fmt.Errorf("failed to get version number: %w", err) + } + + // Insert version + query := ` + INSERT INTO document_versions + (document_id, yjs_snapshot, text_preview, version_number, created_by, version_label, is_auto_generated) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, document_id, text_preview, version_number, created_by, version_label, is_auto_generated, created_at + ` + + var version models.DocumentVersion + err = tx.QueryRowContext(ctx, query, + documentID, snapshot, textPreview, versionNumber, userID, label, isAuto, + ).Scan( + &version.ID, &version.DocumentID, &version.TextPreview, &version.VersionNumber, + &version.CreatedBy, &version.VersionLabel, &version.IsAutoGenerated, &version.CreatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to create version: %w", err) + } + + // Update document metadata + _, err = tx.ExecContext(ctx, ` + UPDATE documents + SET version_count = version_count + 1, + last_snapshot_at = NOW() + WHERE id = $1 + `, documentID) + if err != nil { + return nil, fmt.Errorf("failed to update document metadata: %w", err) + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return &version, nil +} + +// ListDocumentVersions returns paginated version list with author info +func (s *PostgresStore) ListDocumentVersions( + ctx context.Context, + documentID uuid.UUID, + limit int, + offset int, +) ([]models.DocumentVersionWithAuthor, int, error) { + // Get total count + var total int + err := s.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM document_versions WHERE document_id = $1`, + documentID, + ).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to count versions: %w", err) + } + + // Get paginated versions with author (LEFT JOIN to handle deleted users) + query := ` + SELECT + v.id, v.document_id, v.text_preview, v.version_number, + v.created_by, v.version_label, v.is_auto_generated, v.created_at, + u.id, u.email, u.name, u.avatar_url + FROM document_versions v + LEFT JOIN users u ON v.created_by = u.id + WHERE v.document_id = $1 + ORDER BY v.version_number DESC + LIMIT $2 OFFSET $3 + ` + + rows, err := s.db.QueryContext(ctx, query, documentID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list versions: %w", err) + } + defer rows.Close() + + var versions []models.DocumentVersionWithAuthor + for rows.Next() { + var v models.DocumentVersionWithAuthor + var authorID, authorEmail, authorName sql.NullString + var authorAvatar *string + + err := rows.Scan( + &v.ID, &v.DocumentID, &v.TextPreview, &v.VersionNumber, + &v.CreatedBy, &v.VersionLabel, &v.IsAutoGenerated, &v.CreatedAt, + &authorID, &authorEmail, &authorName, &authorAvatar, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan version: %w", err) + } + + // Populate author if exists + if authorID.Valid { + authorUUID, _ := uuid.Parse(authorID.String) + v.Author = &models.User{ + ID: authorUUID, + Email: authorEmail.String, + Name: authorName.String, + AvatarURL: authorAvatar, + } + } + + versions = append(versions, v) + } + + return versions, total, nil +} + +// GetDocumentVersion retrieves a specific version with full snapshot +func (s *PostgresStore) GetDocumentVersion(ctx context.Context, versionID uuid.UUID) (*models.DocumentVersion, error) { + query := ` + SELECT id, document_id, yjs_snapshot, text_preview, ve rsion_number, + created_by, version_label, is_auto_generated, created_at + FROM document_versions + WHERE id = $1 + ` + + var version models.DocumentVersion + err := s.db.QueryRowContext(ctx, query, versionID).Scan( + &version.ID, &version.DocumentID, &version.YjsSnapshot, &version.TextPreview, + &version.VersionNumber, &version.CreatedBy, &version.VersionLabel, + &version.IsAutoGenerated, &version.CreatedAt, + ) + + if err == sql.ErrNoRows { + return nil, fmt.Errorf("version not found") + } + if err != nil { + return nil, fmt.Errorf("failed to get version: %w", err) + } + + return &version, nil +} + +// GetLatestDocumentVersion gets the most recent version for auto-snapshot comparison +func (s *PostgresStore) GetLatestDocumentVersion(ctx context.Context, documentID uuid.UUID) (*models.DocumentVersion, error) { + query := ` + SELECT id, document_id, yjs_snapshot, text_preview, version_number, + created_by, version_label, is_auto_generated, created_at + FROM document_versions + WHERE document_id = $1 + ORDER BY version_number DESC + LIMIT 1 + ` + + var version models.DocumentVersion + err := s.db.QueryRowContext(ctx, query, documentID).Scan( + &version.ID, &version.DocumentID, &version.YjsSnapshot, &version.TextPreview, + &version.VersionNumber, &version.CreatedBy, &version.VersionLabel, + &version.IsAutoGenerated, &version.CreatedAt, + ) + + if err == sql.ErrNoRows { + return nil, nil // No versions yet + } + if err != nil { + return nil, fmt.Errorf("failed to get latest version: %w", err) + } + + return &version, nil +} diff --git a/backend/scripts/000_extensions.sql b/backend/scripts/000_extensions.sql new file mode 100644 index 0000000..6deb054 --- /dev/null +++ b/backend/scripts/000_extensions.sql @@ -0,0 +1,8 @@ +-- Migration: Create required PostgreSQL extensions +-- Extensions must be created before other migrations can use them + +-- uuid-ossp: Provides functions for generating UUIDs (uuid_generate_v4()) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- pgcrypto: Provides cryptographic functions (used for token hashing) +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; diff --git a/backend/scripts/001_init_schema.sql b/backend/scripts/001_init_schema.sql new file mode 100644 index 0000000..66570bd --- /dev/null +++ b/backend/scripts/001_init_schema.sql @@ -0,0 +1,25 @@ +-- Initialize database schema for realtime collaboration +-- This is the base schema that creates core tables for documents and updates + +CREATE TABLE IF NOT EXISTS documents ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL CHECK (type IN ('editor', 'kanban')), + yjs_state BYTEA, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_documents_type ON documents(type); +CREATE INDEX idx_documents_created_at ON documents(created_at DESC); + +-- Table for storing incremental updates (for history tracking) +CREATE TABLE IF NOT EXISTS document_updates ( + id SERIAL PRIMARY KEY, + document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + update BYTEA NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_updates_document_id ON document_updates(document_id); +CREATE INDEX idx_updates_created_at ON document_updates(created_at DESC); diff --git a/backend/scripts/001_add_users_and_sessions.sql b/backend/scripts/002_add_users_and_sessions.sql similarity index 100% rename from backend/scripts/001_add_users_and_sessions.sql rename to backend/scripts/002_add_users_and_sessions.sql diff --git a/backend/scripts/002_add_document_shares.sql b/backend/scripts/003_add_document_shares.sql similarity index 100% rename from backend/scripts/002_add_document_shares.sql rename to backend/scripts/003_add_document_shares.sql diff --git a/backend/scripts/003_add_public_sharing.sql b/backend/scripts/004_add_public_sharing.sql similarity index 100% rename from backend/scripts/003_add_public_sharing.sql rename to backend/scripts/004_add_public_sharing.sql diff --git a/backend/scripts/004_add_share_link_permission.sql b/backend/scripts/005_add_share_link_permission.sql similarity index 100% rename from backend/scripts/004_add_share_link_permission.sql rename to backend/scripts/005_add_share_link_permission.sql diff --git a/backend/scripts/006_add_oauth_tokens.sql b/backend/scripts/006_add_oauth_tokens.sql new file mode 100644 index 0000000..3259130 --- /dev/null +++ b/backend/scripts/006_add_oauth_tokens.sql @@ -0,0 +1,20 @@ +-- Migration: Add OAuth token storage +-- This table stores OAuth2 access tokens and refresh tokens from external providers +-- Used for refreshing user sessions without re-authentication + +CREATE TABLE IF NOT EXISTS oauth_tokens ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider VARCHAR(50) NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT, + token_type VARCHAR(50) DEFAULT 'Bearer', + expires_at TIMESTAMPTZ NOT NULL, + scope TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT oauth_tokens_user_id_provider_key UNIQUE (user_id, provider) +); + +CREATE INDEX idx_oauth_tokens_user_id ON oauth_tokens(user_id); diff --git a/backend/scripts/007_complete_version_migration.sql b/backend/scripts/007_complete_version_migration.sql new file mode 100644 index 0000000..e613323 --- /dev/null +++ b/backend/scripts/007_complete_version_migration.sql @@ -0,0 +1,41 @@ +-- Migration: Add document version history support +-- This migration creates the version history table, adds tracking columns, +-- and provides a helper function for version numbering + +-- Create document versions table for storing version snapshots +CREATE TABLE IF NOT EXISTS document_versions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + yjs_snapshot BYTEA NOT NULL, + text_preview TEXT, + version_number INTEGER NOT NULL, + created_by UUID REFERENCES users(id) ON DELETE SET NULL, + version_label TEXT, + is_auto_generated BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT unique_document_version UNIQUE(document_id, version_number) +); + +CREATE INDEX idx_document_versions_document_id ON document_versions(document_id, created_at DESC); +CREATE INDEX idx_document_versions_created_by ON document_versions(created_by); + +-- Add version tracking columns to documents table +ALTER TABLE documents ADD COLUMN IF NOT EXISTS version_count INTEGER DEFAULT 0; +ALTER TABLE documents ADD COLUMN IF NOT EXISTS last_snapshot_at TIMESTAMPTZ; + +-- Function to get the next version number for a document +-- This ensures version numbers are sequential and unique per document +CREATE OR REPLACE FUNCTION get_next_version_number(p_document_id UUID) +RETURNS INTEGER AS $$ +DECLARE + next_version INTEGER; +BEGIN + SELECT COALESCE(MAX(version_number), 0) + 1 + INTO next_version + FROM document_versions + WHERE document_id = p_document_id; + + RETURN next_version; +END; +$$ LANGUAGE plpgsql; diff --git a/backend/scripts/008_enable_rls.sql b/backend/scripts/008_enable_rls.sql new file mode 100644 index 0000000..0c0b7a9 --- /dev/null +++ b/backend/scripts/008_enable_rls.sql @@ -0,0 +1,36 @@ +-- Migration: Enable Row Level Security (RLS) on all tables +-- This enables RLS but uses permissive policies to allow all operations +-- Authorization is still handled by the Go backend middleware + +-- Enable RLS on all tables +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE sessions ENABLE ROW LEVEL SECURITY; +ALTER TABLE oauth_tokens ENABLE ROW LEVEL SECURITY; +ALTER TABLE documents ENABLE ROW LEVEL SECURITY; +ALTER TABLE document_updates ENABLE ROW LEVEL SECURITY; +ALTER TABLE document_shares ENABLE ROW LEVEL SECURITY; +ALTER TABLE document_versions ENABLE ROW LEVEL SECURITY; + +-- Create permissive policies that allow all operations +-- This maintains current behavior where backend handles authorization + +-- Users table +CREATE POLICY "Allow all operations on users" ON users FOR ALL USING (true); + +-- Sessions table +CREATE POLICY "Allow all operations on sessions" ON sessions FOR ALL USING (true); + +-- OAuth tokens table +CREATE POLICY "Allow all operations on oauth_tokens" ON oauth_tokens FOR ALL USING (true); + +-- Documents table +CREATE POLICY "Allow all operations on documents" ON documents FOR ALL USING (true); + +-- Document updates table +CREATE POLICY "Allow all operations on document_updates" ON document_updates FOR ALL USING (true); + +-- Document shares table +CREATE POLICY "Allow all operations on document_shares" ON document_shares FOR ALL USING (true); + +-- Document versions table +CREATE POLICY "Allow all operations on document_versions" ON document_versions FOR ALL USING (true); diff --git a/backend/scripts/009_disable_postgrest.sql b/backend/scripts/009_disable_postgrest.sql new file mode 100644 index 0000000..d2f1250 --- /dev/null +++ b/backend/scripts/009_disable_postgrest.sql @@ -0,0 +1,16 @@ +-- Migration: Revoke PostgREST access to public schema +-- This prevents Supabase's auto-generated REST API from exposing tables +-- Use this if you ONLY connect via your Go backend, not via Supabase client libraries + +-- Revoke access from anon and authenticated roles (used by PostgREST) +REVOKE ALL ON ALL TABLES IN SCHEMA public FROM anon, authenticated; +REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM anon, authenticated; +REVOKE ALL ON ALL FUNCTIONS IN SCHEMA public FROM anon, authenticated; + +-- Grant access only to postgres role (your backend connection) +GRANT ALL ON ALL TABLES IN SCHEMA public TO postgres; +GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO postgres; +GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO postgres; + +-- Note: Run this AFTER all other migrations +-- If you need PostgREST access later, you can re-grant permissions selectively diff --git a/backend/scripts/init.sql b/backend/scripts/archive/init.sql similarity index 100% rename from backend/scripts/init.sql rename to backend/scripts/archive/init.sql diff --git a/backend/scripts/archive/migration_add_versions.sql b/backend/scripts/archive/migration_add_versions.sql new file mode 100644 index 0000000..3a0c50b --- /dev/null +++ b/backend/scripts/archive/migration_add_versions.sql @@ -0,0 +1,38 @@ +-- Migration: Add document version history support +-- Run: psql -U postgres collaboration < backend/scripts/migration_add_versions.sql + +CREATE TABLE IF NOT EXISTS document_versions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + yjs_snapshot BYTEA NOT NULL, + text_preview TEXT, + version_number INTEGER NOT NULL, + created_by UUID REFERENCES users(id) ON DELETE SET NULL, + version_label TEXT, + is_auto_generated BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT unique_document_version UNIQUE(document_id, version_number) +); + +CREATE INDEX idx_document_versions_document_id ON document_versions(document_id, created_at DESC); +CREATE INDEX idx_document_versions_created_by ON document_versions(created_by); + +-- Add version tracking to documents table +ALTER TABLE documents ADD COLUMN IF NOT EXISTS version_count INTEGER DEFAULT 0; +ALTER TABLE documents ADD COLUMN IF NOT EXISTS last_snapshot_at TIMESTAMPTZ; + +-- Function to get next version number +CREATE OR REPLACE FUNCTION get_next_version_number(p_document_id UUID) +RETURNS INTEGER AS $$ +DECLARE + next_version INTEGER; +BEGIN + SELECT COALESCE(MAX(version_number), 0) + 1 + INTO next_version + FROM document_versions + WHERE document_id = p_document_id; + + RETURN next_version; +END; +$$ LANGUAGE plpgsql; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 0864f18..379e455 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3,11 +3,18 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080/api" export async function authFetch(url: string, options?: RequestInit): Promise { const token = localStorage.getItem('auth_token'); - const headers: HeadersInit = { + const headers: Record = { 'Content-Type': 'application/json', - ...options?.headers, }; + // Merge existing headers if provided + if (options?.headers) { + const existingHeaders = new Headers(options.headers); + existingHeaders.forEach((value, key) => { + headers[key] = value; + }); + } + // Add Authorization header if token exists if (token) { headers['Authorization'] = `Bearer ${token}`; diff --git a/frontend/src/components/Kanban/KanbanBoard.tsx b/frontend/src/components/Kanban/KanbanBoard.tsx index b211e74..709c284 100644 --- a/frontend/src/components/Kanban/KanbanBoard.tsx +++ b/frontend/src/components/Kanban/KanbanBoard.tsx @@ -70,7 +70,7 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => { if (columnIndex !== -1) { providers.ydoc.transact(() => { - const column = cols[columnIndex]; + const column = cols[columnIndex] as KanbanColumn; column.tasks.push(task); yarray.delete(columnIndex, 1); yarray.insert(columnIndex, [column]); @@ -91,8 +91,8 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => { if (fromIndex !== -1 && toIndex !== -1) { providers.ydoc.transact(() => { - const fromCol = { ...cols[fromIndex] }; - const toCol = { ...cols[toIndex] }; + const fromCol = { ...(cols[fromIndex] as KanbanColumn) }; + const toCol = { ...(cols[toIndex] as KanbanColumn) }; const taskIndex = fromCol.tasks.findIndex((t: Task) => t.id === taskId); if (taskIndex !== -1) { diff --git a/frontend/src/components/PixelSprites/FloatingGem.tsx b/frontend/src/components/PixelSprites/FloatingGem.tsx index 0cbd05d..217d1f3 100644 --- a/frontend/src/components/PixelSprites/FloatingGem.tsx +++ b/frontend/src/components/PixelSprites/FloatingGem.tsx @@ -1,4 +1,4 @@ -import { CSSProperties } from 'react'; +import type { CSSProperties } from 'react'; interface FloatingGemProps { position?: { top?: string; right?: string; bottom?: string; left?: string }; diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx index 197ee99..f047b1b 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/components/ProtectedRoute.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; import { Navigate, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 72df492..f709642 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,4 +1,5 @@ -import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect } from 'react'; +import type { ReactNode } from 'react'; import type { User, AuthContextType } from '../types/auth'; import { authApi } from '../api/auth'; diff --git a/frontend/src/hooks/useAutoSave.ts b/frontend/src/hooks/useAutoSave.ts index 8f41daf..a4a80d3 100644 --- a/frontend/src/hooks/useAutoSave.ts +++ b/frontend/src/hooks/useAutoSave.ts @@ -3,13 +3,13 @@ import * as Y from 'yjs'; import { documentsApi } from '../api/document'; export const useAutoSave = (documentId: string, ydoc: Y.Doc | null) => { - const saveTimeoutRef = useRef(null); + const saveTimeoutRef = useRef(null); const isSavingRef = useRef(false); useEffect(() => { if (!ydoc) return; - const handleUpdate = (update: Uint8Array, origin: any) => { + const handleUpdate = (_update: Uint8Array, origin: any) => { // Ignore updates from initial sync or remote sources if (origin === 'init' || origin === 'remote') return; diff --git a/frontend/src/lib/yjs.ts b/frontend/src/lib/yjs.ts index b720d42..086eb1f 100644 --- a/frontend/src/lib/yjs.ts +++ b/frontend/src/lib/yjs.ts @@ -4,7 +4,7 @@ import { WebsocketProvider } from "y-websocket"; import * as Y from "yjs"; import { documentsApi } from "../api/document"; -const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8080/ws"; +const WS_URL = import.meta.env.VITE_WS_URL; export interface YjsProviders { ydoc: Y.Doc; @@ -21,7 +21,7 @@ export interface YjsUser { export const createYjsDocument = async ( documentId: string, - user: YjsUser, + _user: YjsUser, token: string, shareToken?: string ): Promise => { diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index 747d1a3..d8bb4eb 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import Editor from "../components/Editor/Editor.tsx"; +import Navbar from "../components/Navbar.tsx"; import UserList from "../components/Presence/UserList.tsx"; import ShareModal from "../components/Share/ShareModal.tsx"; -import Navbar from "../components/Navbar.tsx"; import { useYjsDocument } from "../hooks/useYjsDocument.ts"; const EditorPage = () => {