First draft
This commit is contained in:
1 parent
aba52f2b3c
commit
84bc6884b0
37 files changed
+6697
-27
No files matched your search
@@ -0,0 +1,368 @@
|
||||
import {
|
||||
APIKey,
|
||||
DashboardStats,
|
||||
Document,
|
||||
DocumentBlock,
|
||||
ListItem,
|
||||
PriorityLevel,
|
||||
SharedList,
|
||||
Space,
|
||||
Thought,
|
||||
User,
|
||||
} from './types';
|
||||
|
||||
const TOKEN_STORAGE_KEY = 'mindsift_jwt_token';
|
||||
const OFFLINE_QUEUE_KEY = 'mindsift_offline_capture_queue';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
export function clearToken(): void {
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function triggerHaptic(pattern: number | number[] = 15): void {
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && 'vibrate' in navigator) {
|
||||
navigator.vibrate(pattern);
|
||||
}
|
||||
} catch {
|
||||
// Ignore on desktop browsers
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(path, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errMsg = `HTTP ${res.status}`;
|
||||
try {
|
||||
const errData = await res.json();
|
||||
if (errData.error) errMsg = errData.error;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// --- AUTH ---
|
||||
async quickStart(): Promise<{ token: string; user: User }> {
|
||||
const res = await request<{ token: string; user: User }>('/api/v1/auth/quickstart', {
|
||||
method: 'POST',
|
||||
});
|
||||
setToken(res.token);
|
||||
return res;
|
||||
},
|
||||
|
||||
async login(email: string, password: string): Promise<{ token: string; user: User }> {
|
||||
const res = await request<{ token: string; user: User }>('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
setToken(res.token);
|
||||
return res;
|
||||
},
|
||||
|
||||
async register(email: string, password: string, displayName: string): Promise<{ token: string; user: User }> {
|
||||
const res = await request<{ token: string; user: User }>('/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password, displayName }),
|
||||
});
|
||||
setToken(res.token);
|
||||
return res;
|
||||
},
|
||||
|
||||
async getMe(): Promise<{ user: User; stats: DashboardStats }> {
|
||||
return request<{ user: User; stats: DashboardStats }>('/api/v1/auth/me');
|
||||
},
|
||||
|
||||
async getStats(): Promise<DashboardStats> {
|
||||
return request<DashboardStats>('/api/v1/stats');
|
||||
},
|
||||
|
||||
// --- INSTANT CAPTURE & OFFLINE SYNC ---
|
||||
async captureThought(
|
||||
content: string,
|
||||
priority: PriorityLevel = 'normal',
|
||||
source: 'manual' | 'voice' = 'manual',
|
||||
dueDate = ''
|
||||
): Promise<Thought> {
|
||||
try {
|
||||
const thought = await request<Thought>('/api/v1/inbox', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content, priority, source, dueDate }),
|
||||
});
|
||||
return thought;
|
||||
} catch (err) {
|
||||
// Queue locally if offline
|
||||
const queue = getOfflineQueue();
|
||||
const offlineThought: Thought = {
|
||||
id: `th_offline_${Date.now()}`,
|
||||
userId: 'offline',
|
||||
content,
|
||||
tags: (content.match(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g) || []).map((t) => t.slice(1).toLowerCase()),
|
||||
priority,
|
||||
source,
|
||||
status: 'inbox',
|
||||
isCompleted: false,
|
||||
dueDate,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
queue.push(offlineThought);
|
||||
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
|
||||
return offlineThought;
|
||||
}
|
||||
},
|
||||
|
||||
async flushOfflineQueue(): Promise<number> {
|
||||
const queue = getOfflineQueue();
|
||||
if (queue.length === 0) return 0;
|
||||
|
||||
let synced = 0;
|
||||
const remaining: Thought[] = [];
|
||||
|
||||
for (const item of queue) {
|
||||
try {
|
||||
await request<Thought>('/api/v1/inbox', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: item.content,
|
||||
priority: item.priority,
|
||||
source: item.source,
|
||||
dueDate: item.dueDate || '',
|
||||
}),
|
||||
});
|
||||
synced++;
|
||||
} catch {
|
||||
remaining.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(remaining));
|
||||
return synced;
|
||||
},
|
||||
|
||||
// --- THOUGHTS, TRIAGE & TASKS ---
|
||||
async getTriageDeck(): Promise<Thought[]> {
|
||||
return request<Thought[]>('/api/v1/triage/deck');
|
||||
},
|
||||
|
||||
async getThoughts(status = 'all'): Promise<Thought[]> {
|
||||
return request<Thought[]>(`/api/v1/thoughts?status=${encodeURIComponent(status)}`);
|
||||
},
|
||||
|
||||
async triageAction(
|
||||
thoughtId: string,
|
||||
action: 'keep' | 'discard' | 'restore',
|
||||
options?: {
|
||||
convertTo?: 'task' | 'document' | 'list_item';
|
||||
targetListId?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
): Promise<Thought> {
|
||||
return request<Thought>(`/api/v1/triage/${thoughtId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
action,
|
||||
convertTo: options?.convertTo || '',
|
||||
targetListId: options?.targetListId || '',
|
||||
dueDate: options?.dueDate || '',
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
async getTasks(): Promise<Thought[]> {
|
||||
return request<Thought[]>('/api/v1/tasks');
|
||||
},
|
||||
|
||||
async updateThought(
|
||||
thoughtId: string,
|
||||
patch: { content?: string; isCompleted?: boolean; dueDate?: string; priority?: PriorityLevel }
|
||||
): Promise<Thought> {
|
||||
return request<Thought>(`/api/v1/thoughts/${thoughtId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteThought(thoughtId: string): Promise<void> {
|
||||
await request(`/api/v1/thoughts/${thoughtId}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async purgeDiscarded(): Promise<{ purged: number }> {
|
||||
return request<{ purged: number }>('/api/v1/thoughts/discarded/purge', { method: 'DELETE' });
|
||||
},
|
||||
|
||||
// --- WORKSPACE DOCUMENTS ---
|
||||
async getDocuments(): Promise<Document[]> {
|
||||
return request<Document[]>('/api/v1/documents');
|
||||
},
|
||||
|
||||
async getDocument(id: string): Promise<Document> {
|
||||
return request<Document>(`/api/v1/documents/${id}`);
|
||||
},
|
||||
|
||||
async createDocument(payload: {
|
||||
title: string;
|
||||
icon?: string;
|
||||
blocks?: DocumentBlock[];
|
||||
tags?: string[];
|
||||
isPinned?: boolean;
|
||||
}): Promise<Document> {
|
||||
return request<Document>('/api/v1/documents', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
|
||||
async updateDocument(
|
||||
id: string,
|
||||
patch: {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
blocks?: DocumentBlock[];
|
||||
tags?: string[];
|
||||
isPinned?: boolean;
|
||||
}
|
||||
): Promise<Document> {
|
||||
return request<Document>(`/api/v1/documents/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteDocument(id: string): Promise<void> {
|
||||
await request(`/api/v1/documents/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
// --- SPACES & REALTIME LISTS ---
|
||||
async getSpaces(): Promise<Space[]> {
|
||||
return request<Space[]>('/api/v1/spaces');
|
||||
},
|
||||
|
||||
async createSpace(name: string, icon: string, description: string): Promise<Space> {
|
||||
return request<Space>('/api/v1/spaces', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, icon, description }),
|
||||
});
|
||||
},
|
||||
|
||||
async joinSpace(inviteCode: string): Promise<Space> {
|
||||
return request<Space>('/api/v1/spaces/join', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ inviteCode }),
|
||||
});
|
||||
},
|
||||
|
||||
async getSpaceLists(spaceId: string): Promise<SharedList[]> {
|
||||
return request<SharedList[]>(`/api/v1/spaces/${spaceId}/lists`);
|
||||
},
|
||||
|
||||
async getAllLists(): Promise<SharedList[]> {
|
||||
return request<SharedList[]>('/api/v1/lists');
|
||||
},
|
||||
|
||||
async createList(spaceId: string, title: string, icon: string, listType: 'shopping' | 'checklist' | 'vacation'): Promise<SharedList> {
|
||||
return request<SharedList>(`/api/v1/spaces/${spaceId}/lists`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, icon, listType }),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteList(listId: string): Promise<void> {
|
||||
await request(`/api/v1/lists/${listId}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async createListItem(listId: string, content: string, category = '', quantity = ''): Promise<ListItem> {
|
||||
return request<ListItem>(`/api/v1/lists/${listId}/items`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content, category, quantity }),
|
||||
});
|
||||
},
|
||||
|
||||
async toggleListItem(listId: string, itemId: string, isCompleted: boolean): Promise<ListItem> {
|
||||
return request<ListItem>(`/api/v1/lists/${listId}/items/${itemId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isCompleted }),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteListItem(listId: string, itemId: string): Promise<void> {
|
||||
await request(`/api/v1/lists/${listId}/items/${itemId}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async clearCompletedItems(listId: string): Promise<{ removed: number }> {
|
||||
return request<{ removed: number }>(`/api/v1/lists/${listId}/clear-completed`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
|
||||
// --- API KEYS, WEBHOOKS & EXPORT ---
|
||||
async getAPIKeys(): Promise<APIKey[]> {
|
||||
return request<APIKey[]>('/api/v1/keys');
|
||||
},
|
||||
|
||||
async createAPIKey(name: string): Promise<APIKey> {
|
||||
return request<APIKey>('/api/v1/keys', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteAPIKey(id: string): Promise<void> {
|
||||
await request(`/api/v1/keys/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async testWebhookIngest(token: string, title: string, priority: PriorityLevel, tags: string[]): Promise<{ status: string; thought: Thought }> {
|
||||
return request<{ status: string; thought: Thought }>(`/api/v1/webhooks/ingest?token=${encodeURIComponent(token)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, priority, tags }),
|
||||
});
|
||||
},
|
||||
|
||||
async searchAll(q: string): Promise<{ thoughts: Thought[]; documents: Document[] }> {
|
||||
return request<{ thoughts: Thought[]; documents: Document[] }>(`/api/v1/search?q=${encodeURIComponent(q)}`);
|
||||
},
|
||||
|
||||
async exportAll(): Promise<any> {
|
||||
return request<any>('/api/v1/export');
|
||||
},
|
||||
};
|
||||
|
||||
export function getOfflineQueue(): Thought[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(OFFLINE_QUEUE_KEY);
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function buildWebSocketURL(spaceId: string): string {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const token = getToken() || '';
|
||||
return `${protocol}//${window.location.host}/api/v1/ws/spaces/${encodeURIComponent(spaceId)}?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
export type ThoughtStatus = 'inbox' | 'kept' | 'discarded' | 'converted';
|
||||
export type PriorityLevel = 'low' | 'normal' | 'high' | 'urgent';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Thought {
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
priority: PriorityLevel;
|
||||
source: 'manual' | 'voice' | 'webhook';
|
||||
status: ThoughtStatus;
|
||||
isCompleted: boolean;
|
||||
convertedType?: 'task' | 'document' | 'list_item';
|
||||
convertedRefId?: string;
|
||||
dueDate?: string;
|
||||
createdAt: string;
|
||||
triagedAt?: string;
|
||||
}
|
||||
|
||||
export type BlockType = 'h1' | 'h2' | 'h3' | 'paragraph' | 'todo' | 'bullet' | 'code' | 'callout' | 'divider';
|
||||
|
||||
export interface DocumentBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
content: string;
|
||||
checked?: boolean;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
id: string;
|
||||
userId: string;
|
||||
spaceId?: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
content: string;
|
||||
blocks: DocumentBlock[];
|
||||
tags: string[];
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SpaceMember {
|
||||
spaceId: string;
|
||||
userId: string;
|
||||
displayName: string;
|
||||
email: string;
|
||||
role: 'owner' | 'editor' | 'viewer';
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
export interface Space {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
inviteCode: string;
|
||||
isPersonal: boolean;
|
||||
ownerId: string;
|
||||
members?: SpaceMember[];
|
||||
listsCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
id: string;
|
||||
listId: string;
|
||||
content: string;
|
||||
category?: string;
|
||||
quantity?: string;
|
||||
isCompleted: boolean;
|
||||
completedBy?: string;
|
||||
completedByName?: string;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export interface SharedList {
|
||||
id: string;
|
||||
spaceId: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
listType: 'shopping' | 'checklist' | 'vacation';
|
||||
items: ListItem[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface APIKey {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
keyPrefix: string;
|
||||
rawKey?: string;
|
||||
scopes: string;
|
||||
lastUsedAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
inboxCount: number;
|
||||
tasksOpenCount: number;
|
||||
tasksDoneCount: number;
|
||||
docsCount: number;
|
||||
listsCount: number;
|
||||
discardedCount: number;
|
||||
}
|
||||
|
||||
export interface WSMessage<T = any> {
|
||||
event: 'ITEM_CREATED' | 'ITEM_UPDATED' | 'ITEM_TOGGLED' | 'ITEM_DELETED' | 'LIST_CLEARED' | 'LIST_CREATED' | 'LIST_DELETED' | 'USER_PRESENCE';
|
||||
space_id: string;
|
||||
list_id?: string;
|
||||
payload: T;
|
||||
timestamp: string;
|
||||
}
|
||||
Reference in new issue
Block a user