Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
456 changes: 260 additions & 196 deletions .svelte-kit/ambient.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .svelte-kit/generated/server/internal.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified server/database/notevault.db
Binary file not shown.
70 changes: 65 additions & 5 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,43 @@ type RequestInit = {
credentials?: RequestCredentials;
};

const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:12001/api';

class ApiClient {
private cache = new Map<string, { data: any; timestamp: number }>();
private readonly CACHE_TTL = 5000; // 5 seconds cache

private getAuthHeaders(): HeadersInit {
if (!browser) return {};

const token = localStorage.getItem('auth_token');
return token ? { Authorization: `Bearer ${token}` } : {};
}

private getCacheKey(endpoint: string, options: RequestInit = {}): string {
return `${options.method || 'GET'}:${endpoint}`;
}

private isValidCache(timestamp: number): boolean {
return Date.now() - timestamp < this.CACHE_TTL;
}

private async request<T>(
endpoint: string,
options: RequestInit = {}
options: RequestInit = {},
retryCount = 0
): Promise<T> {
const cacheKey = this.getCacheKey(endpoint, options);
const method = options.method || 'GET';

// Only cache GET requests
if (method === 'GET') {
const cached = this.cache.get(cacheKey);
if (cached && this.isValidCache(cached.timestamp)) {
return cached.data;
}
}

const url = `${API_BASE_URL}${endpoint}`;

const config: RequestInit = {
Expand All @@ -39,11 +62,35 @@ class ApiClient {
const response = await fetch(url, config);

if (!response.ok) {
// Handle rate limiting with exponential backoff
if (response.status === 429 && retryCount < 3) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, retryCount) * 1000;

console.warn(`Rate limited. Retrying after ${delay}ms (attempt ${retryCount + 1}/3)`);

await new Promise(resolve => setTimeout(resolve, delay));
return this.request<T>(endpoint, options, retryCount + 1);
}

const error = await response.json().catch(() => ({ error: 'Network error' }));

// Provide user-friendly error messages for rate limiting
if (response.status === 429) {
throw new Error('Too many requests. Please wait a moment and try again.');
}

throw new Error(error.error || `HTTP ${response.status}`);
}

return await response.json();
const data = await response.json();

// Cache GET requests
if (method === 'GET') {
this.cache.set(cacheKey, { data, timestamp: Date.now() });
}

return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
Expand Down Expand Up @@ -149,8 +196,20 @@ class ApiClient {
}

// Note endpoints
async getWorkspaceNotes(workspaceId: string) {
return this.request(`/notes/workspace/${workspaceId}`);
async getWorkspaceNotes(workspaceId: string, params?: {
limit?: number;
offset?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}) {
const query = new URLSearchParams();
if (params?.limit) query.set('limit', params.limit.toString());
if (params?.offset) query.set('offset', params.offset.toString());
if (params?.sortBy) query.set('sortBy', params.sortBy);
if (params?.sortOrder) query.set('sortOrder', params.sortOrder);

const url = `/notes/workspace/${workspaceId}${query.toString() ? '?' + query.toString() : ''}`;
return this.request(url);
}

async getNote(id: string) {
Expand All @@ -167,6 +226,7 @@ class ApiClient {
color: string;
tags?: string[];
isPublic?: boolean;
collectionId?: string;
}) {
return this.request('/notes', {
method: 'POST',
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/ChatMembersModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
}

$: filteredUsers = $onlineUsers.filter(user =>
user.displayName.toLowerCase().includes(searchQuery.toLowerCase()) ||
(user.displayName || user.username || '')?.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.username?.toLowerCase().includes(searchQuery.toLowerCase())
);
</script>
Expand Down Expand Up @@ -111,7 +111,7 @@
<div class="relative">
<img
src={user.avatar}
alt={user.displayName}
alt={user.displayName || user.username}
class="w-10 h-10 rounded-full"
/>
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-dark-900 rounded-full"></div>
Expand All @@ -120,7 +120,7 @@
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2">
<span class="font-medium text-white truncate">
{user.displayName}
{user.displayName || user.username}
</span>
{#if getRoleIcon(user.role)}
<svelte:component
Expand Down
7 changes: 5 additions & 2 deletions src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
}
return $page.url.pathname.startsWith(href);
}

// Reactive statement to debug active states
$: console.log('Current page path:', $page.url.pathname);

function handleLogout() {
authStore.logout();
Expand Down Expand Up @@ -93,7 +96,7 @@
{#each navigation as item}
<a
href={item.href}
class={isActive(item.href) ? 'sidebar-item-active' : 'sidebar-item-inactive'}
class="flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors {isActive(item.href) ? 'bg-primary-600 text-white' : 'text-dark-300 hover:bg-dark-800 hover:text-white'}"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center">
Expand Down Expand Up @@ -133,7 +136,7 @@
{#each adminNavigation as item}
<a
href={item.href}
class={isActive(item.href) ? 'sidebar-item-active' : 'sidebar-item-inactive'}
class="flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors {isActive(item.href) ? 'bg-primary-600 text-white' : 'text-dark-300 hover:bg-dark-800 hover:text-white'}"
>
<svelte:component this={item.icon} class="w-5 h-5 mr-3" />
{item.name}
Expand Down
7 changes: 5 additions & 2 deletions src/lib/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ const getCurrentUserId = (): string | null => {

export const chatStore = {
connect: () => {
if (!browser || socket?.connected) return;
if (!browser) return;

// If there's already a connected socket, don't create a new one
if (socket && socket.connected) return;

// Use the same base URL as the API but without the /api path for WebSocket
const wsUrl = import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL.replace('/api', '')
: 'http://localhost:3001';
: 'http://localhost:12001';

socket = io(wsUrl, {
transports: ['websocket', 'polling']
Expand Down
26 changes: 22 additions & 4 deletions src/lib/stores/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,16 @@ export const workspaceStore = {
}
},

loadWorkspaceNotes: async (workspaceId: string) => {
loadWorkspaceNotes: async (workspaceId: string, params?: {
limit?: number;
offset?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
append?: boolean;
}) => {
try {
const notesData = await api.getWorkspaceNotes(workspaceId);
const { append = false, ...queryParams } = params || {};
const notesData = await api.getWorkspaceNotes(workspaceId, queryParams);
const formattedNotes: Note[] = notesData.map((note: any) => ({
id: note.id,
title: note.title,
Expand All @@ -69,10 +76,20 @@ export const workspaceStore = {
updatedAt: new Date(note.updatedAt),
isPublic: note.isPublic
}));
workspaceNotes.set(formattedNotes);

if (append) {
workspaceNotes.update(notes => [...notes, ...formattedNotes]);
} else {
workspaceNotes.set(formattedNotes);
}

return formattedNotes;
} catch (error) {
console.error('Failed to load workspace notes:', error);
workspaceNotes.set([]);
if (!params?.append) {
workspaceNotes.set([]);
}
return [];
}
},

Expand Down Expand Up @@ -210,6 +227,7 @@ export const workspaceStore = {
color: string;
tags?: string[];
isPublic?: boolean;
collectionId?: string;
}) => {
try {
const newNoteData = await api.createNote({
Expand Down
16 changes: 14 additions & 2 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import { authStore, isAuthenticated, isLoading } from '$lib/stores/auth';
import { showCreateWorkspaceModal } from '$lib/stores/modals';
import { goto } from '$app/navigation';
import { navigating } from '$app/stores';
import { chatStore } from '$lib/stores/chat';
import Sidebar from '$lib/components/Sidebar.svelte';
import CommandPalette from '$lib/components/CommandPalette.svelte';
import CreateWorkspaceModal from '$lib/components/CreateWorkspaceModal.svelte';
Expand All @@ -26,8 +28,18 @@
let rightPanelVisible = false;
let focusModeActive = false;

let initialized = false;

onMount(async () => {
authStore.checkAuth();
if (!initialized) {
initialized = true;
await authStore.checkAuth();

// Initialize chat connection only once after auth is checked
if (browser) {
chatStore.connect();
}
}

if (browser) {
// Initialize PWA functionality
Expand Down Expand Up @@ -291,7 +303,7 @@
}

$: {
if (!$isLoading && !$isAuthenticated && !publicRoutes.includes($page.url.pathname)) {
if (initialized && !$isLoading && !$isAuthenticated && !publicRoutes.includes($page.url.pathname)) {
goto('/login');
}
}
Expand Down
1 change: 0 additions & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

onMount(async () => {
loadWorkspacesWithLoading();
chatStore.connect();

// Load real data
await loadDashboardData();
Expand Down
12 changes: 1 addition & 11 deletions src/routes/chat/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
const emojis = ['😀', '😂', '😍', '🤔', '👍', '👎', '❤️', '🎉', '🔥', '💯'];

onMount(() => {
if (!$isConnected) {
chatStore.connect();
}
// Load public chat messages only (exclude workspace channels)
chatStore.loadMessages({ channel: 'public' });
});
Expand Down Expand Up @@ -157,14 +154,7 @@
bind:this={messagesContainer}
class="flex-1 overflow-y-auto px-4 py-4 space-y-1"
>
{#if !$isConnected}
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500 mx-auto mb-4"></div>
<p class="text-dark-400">Connecting to chat...</p>
</div>
</div>
{:else if $chatMessages.length === 0}
{#if $chatMessages.length === 0}
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div class="w-16 h-16 bg-dark-800 rounded-full flex items-center justify-center mx-auto mb-4">
Expand Down
Loading
Loading