ArunKr commited on
Commit
a71b811
·
verified ·
1 Parent(s): cc7e11f

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. static/dashboard.html +263 -29
  2. supabase_notes.sql +61 -0
static/dashboard.html CHANGED
@@ -650,17 +650,41 @@
650
  <div class="pt-4 border-t border-gray-700">
651
  <div class="flex items-center justify-between gap-2 mb-2">
652
  <div class="font-semibold text-gray-200">Notes</div>
653
- <button onclick="saveUserNotes()"
654
- class="bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs">Save</button>
 
 
 
 
 
 
655
  </div>
656
- <div class="text-xs text-gray-400 mb-2">Personal markdown notes (saved to Supabase).</div>
657
- <div class="grid grid-cols-1 gap-3">
658
- <textarea id="notes-editor" rows="8"
659
- class="w-full bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500 font-mono"
660
- placeholder="# Notes..."></textarea>
661
- <div class="text-xs text-gray-400">Preview</div>
662
- <div id="notes-preview" class="prose prose-invert max-w-none bg-gray-900/40 border border-gray-700 rounded-lg p-3 overflow-auto"></div>
663
- <div id="notes-status" class="text-xs text-gray-500"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  </div>
665
  </div>
666
  </div>
@@ -2600,44 +2624,254 @@
2600
  }
2601
  }
2602
 
2603
- async function initNotes() {
2604
- const editor = document.getElementById('notes-editor');
2605
- const preview = document.getElementById('notes-preview');
 
 
 
2606
  const status = document.getElementById('notes-status');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2607
  if (!editor || !preview) return;
 
 
2608
 
2609
- editor.addEventListener('input', () => {
2610
- renderMarkdownInto(preview, editor.value || '');
2611
- if (status) status.textContent = 'Not saved';
 
 
 
2612
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2614
  try {
2615
  const { data: { user } } = await supabase.auth.getUser();
2616
  if (!user) return;
2617
- const { data, error } = await supabase.from('user_notes').select('content,updated_at').eq('user_id', user.id).maybeSingle();
 
 
 
 
 
2618
  if (error) throw error;
2619
- editor.value = data?.content || '';
2620
- renderMarkdownInto(preview, editor.value || '');
2621
- if (status) status.textContent = data?.updated_at ? `Loaded (${new Date(data.updated_at).toLocaleString()})` : 'No notes yet';
2622
  } catch (e) {
2623
- if (status) status.textContent = 'Notes table not configured yet.';
2624
  console.warn('Notes load failed', e);
2625
  }
2626
  }
2627
 
2628
- async function saveUserNotes() {
2629
- const editor = document.getElementById('notes-editor');
2630
- const status = document.getElementById('notes-status');
2631
- if (!editor) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2632
  try {
2633
  const { data: { user } } = await supabase.auth.getUser();
2634
  if (!user) return;
2635
- const content = editor.value || '';
2636
- const { error } = await supabase.from('user_notes').upsert({ user_id: user.id, content }, { onConflict: 'user_id' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2637
  if (error) throw error;
2638
- if (status) status.textContent = `Saved (${new Date().toLocaleString()})`;
 
 
 
 
 
 
 
2639
  } catch (e) {
2640
- if (status) status.textContent = `Save failed: ${e?.message || e}`;
2641
  }
2642
  }
2643
 
 
650
  <div class="pt-4 border-t border-gray-700">
651
  <div class="flex items-center justify-between gap-2 mb-2">
652
  <div class="font-semibold text-gray-200">Notes</div>
653
+ <div class="flex gap-2">
654
+ <button onclick="createNotesFolder()"
655
+ class="bg-gray-700 hover:bg-gray-600 text-white px-2 py-1 rounded text-xs">+ Folder</button>
656
+ <button onclick="createNotesDoc()"
657
+ class="bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs">+ Note</button>
658
+ <button onclick="saveActiveNote()"
659
+ class="bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs">Save</button>
660
+ </div>
661
  </div>
662
+ <div class="text-xs text-gray-400 mb-2">Markdown notes with folders (saved to Supabase).</div>
663
+
664
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
665
+ <div class="bg-gray-900/40 border border-gray-700 rounded-lg p-2 overflow-auto" style="max-height: 340px;">
666
+ <div class="flex items-center justify-between mb-2">
667
+ <div class="text-xs text-gray-400">Folders</div>
668
+ <button onclick="refreshNotesTree()" class="text-xs text-gray-300 hover:text-white"></button>
669
+ </div>
670
+ <div id="notes-tree" class="text-sm space-y-1"></div>
671
+ </div>
672
+ <div class="space-y-2">
673
+ <input id="notes-title" type="text" placeholder="Title"
674
+ class="w-full bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500" />
675
+ <textarea id="notes-editor" rows="8"
676
+ class="w-full bg-gray-700 text-sm rounded border border-gray-600 p-2 text-white outline-none focus:border-blue-500 font-mono"
677
+ placeholder="# Notes..."></textarea>
678
+ <div class="text-xs text-gray-400">Preview</div>
679
+ <div id="notes-preview" class="prose prose-invert max-w-none bg-gray-900/40 border border-gray-700 rounded-lg p-3 overflow-auto" style="max-height: 180px;"></div>
680
+ <div class="flex items-center justify-between">
681
+ <div id="notes-status" class="text-xs text-gray-500"></div>
682
+ <div class="flex gap-2">
683
+ <button onclick="renameActiveNote()" class="text-xs text-gray-300 hover:text-white">Rename</button>
684
+ <button onclick="deleteActiveNote()" class="text-xs text-red-300 hover:text-red-200">Delete</button>
685
+ </div>
686
+ </div>
687
+ </div>
688
  </div>
689
  </div>
690
  </div>
 
2624
  }
2625
  }
2626
 
2627
+ // --- Notes (folders + markdown docs) ---
2628
+ let notesItems = []; // flat items from DB
2629
+ let activeNoteId = null;
2630
+ let activeFolderId = null;
2631
+
2632
+ function setNotesStatus(msg) {
2633
  const status = document.getElementById('notes-status');
2634
+ if (status) status.textContent = msg || '';
2635
+ }
2636
+
2637
+ function getNotesInputs() {
2638
+ return {
2639
+ title: document.getElementById('notes-title'),
2640
+ editor: document.getElementById('notes-editor'),
2641
+ preview: document.getElementById('notes-preview'),
2642
+ tree: document.getElementById('notes-tree'),
2643
+ };
2644
+ }
2645
+
2646
+ function renderNotesPreview() {
2647
+ const { editor, preview } = getNotesInputs();
2648
  if (!editor || !preview) return;
2649
+ renderMarkdownInto(preview, editor.value || '');
2650
+ }
2651
 
2652
+ function buildNotesTree() {
2653
+ const byParent = new Map();
2654
+ notesItems.forEach((item) => {
2655
+ const key = item.parent_id || 'root';
2656
+ if (!byParent.has(key)) byParent.set(key, []);
2657
+ byParent.get(key).push(item);
2658
  });
2659
+ byParent.forEach((arr) => arr.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || (a.title || '').localeCompare(b.title || '')));
2660
+ return byParent;
2661
+ }
2662
+
2663
+ function renderNotesTree() {
2664
+ const { tree } = getNotesInputs();
2665
+ if (!tree) return;
2666
+ const byParent = buildNotesTree();
2667
+
2668
+ const makeNode = (item, depth) => {
2669
+ const row = document.createElement('div');
2670
+ row.className = 'flex items-center gap-2 py-1 px-2 rounded hover:bg-white/10';
2671
+ row.style.marginLeft = `${depth * 10}px`;
2672
+
2673
+ const icon = document.createElement('span');
2674
+ icon.className = 'text-gray-400';
2675
+ icon.textContent = item.kind === 'folder' ? '▸' : '📝';
2676
+
2677
+ const name = document.createElement('button');
2678
+ name.type = 'button';
2679
+ name.className = 'flex-grow text-left truncate';
2680
+ name.textContent = item.title || (item.kind === 'folder' ? 'Untitled folder' : 'Untitled note');
2681
+ name.onclick = () => {
2682
+ if (item.kind === 'folder') {
2683
+ activeFolderId = item.id;
2684
+ // Keep active note selection if inside folder; otherwise clear note selection.
2685
+ setNotesStatus(`Folder: ${item.title || 'Untitled'}`);
2686
+ renderNotesTree();
2687
+ } else {
2688
+ openNote(item.id);
2689
+ }
2690
+ };
2691
 
2692
+ const actions = document.createElement('div');
2693
+ actions.className = 'flex gap-1 shrink-0 opacity-0 hover:opacity-100';
2694
+
2695
+ const del = document.createElement('button');
2696
+ del.type = 'button';
2697
+ del.className = 'text-xs text-red-300 hover:text-red-200';
2698
+ del.textContent = 'Del';
2699
+ del.onclick = (e) => { e.stopPropagation(); deleteNoteItem(item.id); };
2700
+
2701
+ actions.appendChild(del);
2702
+
2703
+ row.appendChild(icon);
2704
+ row.appendChild(name);
2705
+ row.appendChild(actions);
2706
+ return row;
2707
+ };
2708
+
2709
+ const renderBranch = (parentKey, depth) => {
2710
+ const items = byParent.get(parentKey) || [];
2711
+ const frag = document.createDocumentFragment();
2712
+ items.forEach((item) => {
2713
+ const row = makeNode(item, depth);
2714
+ if (item.id === activeFolderId || item.id === activeNoteId) row.classList.add('bg-white/10');
2715
+ frag.appendChild(row);
2716
+ if (item.kind === 'folder') {
2717
+ // simple always-expanded tree
2718
+ frag.appendChild(renderBranch(item.id, depth + 1));
2719
+ }
2720
+ });
2721
+ return frag;
2722
+ };
2723
+
2724
+ tree.innerHTML = '';
2725
+ tree.appendChild(renderBranch('root', 0));
2726
+ }
2727
+
2728
+ async function refreshNotesTree() {
2729
  try {
2730
  const { data: { user } } = await supabase.auth.getUser();
2731
  if (!user) return;
2732
+ const { data, error } = await supabase
2733
+ .from('note_items')
2734
+ .select('id,user_id,parent_id,kind,title,content,sort_order,updated_at')
2735
+ .eq('user_id', user.id)
2736
+ .order('sort_order', { ascending: true })
2737
+ .order('title', { ascending: true });
2738
  if (error) throw error;
2739
+ notesItems = data || [];
2740
+ renderNotesTree();
 
2741
  } catch (e) {
2742
+ setNotesStatus('Notes table not configured yet.');
2743
  console.warn('Notes load failed', e);
2744
  }
2745
  }
2746
 
2747
+ function openNote(id) {
2748
+ const { title, editor } = getNotesInputs();
2749
+ const note = notesItems.find((n) => n.id === id && n.kind === 'note');
2750
+ if (!note || !title || !editor) return;
2751
+ activeNoteId = id;
2752
+ activeFolderId = note.parent_id || null;
2753
+ title.value = note.title || '';
2754
+ editor.value = note.content || '';
2755
+ renderNotesPreview();
2756
+ setNotesStatus(note.updated_at ? `Loaded (${new Date(note.updated_at).toLocaleString()})` : 'Loaded');
2757
+ renderNotesTree();
2758
+ }
2759
+
2760
+ async function initNotes() {
2761
+ const { editor, preview } = getNotesInputs();
2762
+ if (!editor || !preview) return;
2763
+
2764
+ editor.addEventListener('input', () => {
2765
+ renderNotesPreview();
2766
+ setNotesStatus('Not saved');
2767
+ });
2768
+ const titleEl = document.getElementById('notes-title');
2769
+ if (titleEl) {
2770
+ titleEl.addEventListener('input', () => setNotesStatus('Not saved'));
2771
+ }
2772
+
2773
+ await refreshNotesTree();
2774
+ // Auto-open most recently updated note if any
2775
+ const latest = notesItems.filter(i => i.kind === 'note').sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))[0];
2776
+ if (latest) openNote(latest.id);
2777
+ }
2778
+
2779
+ async function createNotesFolder() {
2780
+ const name = prompt('Folder name:');
2781
+ if (!name) return;
2782
+ try {
2783
+ const { data: { user } } = await supabase.auth.getUser();
2784
+ if (!user) return;
2785
+ const { error } = await supabase.from('note_items').insert({
2786
+ user_id: user.id,
2787
+ parent_id: activeFolderId || null,
2788
+ kind: 'folder',
2789
+ title: name,
2790
+ content: '',
2791
+ sort_order: 0
2792
+ });
2793
+ if (error) throw error;
2794
+ await refreshNotesTree();
2795
+ setNotesStatus('Folder created');
2796
+ } catch (e) {
2797
+ setNotesStatus(`Create failed: ${e?.message || e}`);
2798
+ }
2799
+ }
2800
+
2801
+ async function createNotesDoc() {
2802
+ const name = prompt('Note title:');
2803
+ if (!name) return;
2804
  try {
2805
  const { data: { user } } = await supabase.auth.getUser();
2806
  if (!user) return;
2807
+ const { data, error } = await supabase.from('note_items').insert({
2808
+ user_id: user.id,
2809
+ parent_id: activeFolderId || null,
2810
+ kind: 'note',
2811
+ title: name,
2812
+ content: '',
2813
+ sort_order: 0
2814
+ }).select().single();
2815
+ if (error) throw error;
2816
+ await refreshNotesTree();
2817
+ if (data?.id) openNote(data.id);
2818
+ setNotesStatus('Note created');
2819
+ } catch (e) {
2820
+ setNotesStatus(`Create failed: ${e?.message || e}`);
2821
+ }
2822
+ }
2823
+
2824
+ async function saveActiveNote() {
2825
+ const { title, editor } = getNotesInputs();
2826
+ if (!title || !editor) return;
2827
+ if (!activeNoteId) {
2828
+ await createNotesDoc();
2829
+ return;
2830
+ }
2831
+ try {
2832
+ const { error } = await supabase.from('note_items').update({
2833
+ title: title.value || '',
2834
+ content: editor.value || ''
2835
+ }).eq('id', activeNoteId);
2836
+ if (error) throw error;
2837
+ await refreshNotesTree();
2838
+ setNotesStatus(`Saved (${new Date().toLocaleString()})`);
2839
+ } catch (e) {
2840
+ setNotesStatus(`Save failed: ${e?.message || e}`);
2841
+ }
2842
+ }
2843
+
2844
+ async function renameActiveNote() {
2845
+ if (!activeNoteId) return;
2846
+ const { title } = getNotesInputs();
2847
+ const name = prompt('New title:', title?.value || '');
2848
+ if (!name) return;
2849
+ if (title) title.value = name;
2850
+ await saveActiveNote();
2851
+ }
2852
+
2853
+ async function deleteActiveNote() {
2854
+ if (!activeNoteId) return;
2855
+ await deleteNoteItem(activeNoteId);
2856
+ }
2857
+
2858
+ async function deleteNoteItem(id) {
2859
+ const item = notesItems.find((n) => n.id === id);
2860
+ if (!item) return;
2861
+ if (!confirm(`Delete "${item.title || 'Untitled'}"? This deletes children too.`)) return;
2862
+ try {
2863
+ const { error } = await supabase.from('note_items').delete().eq('id', id);
2864
  if (error) throw error;
2865
+ if (activeNoteId === id) activeNoteId = null;
2866
+ if (activeFolderId === id) activeFolderId = null;
2867
+ const { title, editor } = getNotesInputs();
2868
+ if (title) title.value = '';
2869
+ if (editor) editor.value = '';
2870
+ renderNotesPreview();
2871
+ await refreshNotesTree();
2872
+ setNotesStatus('Deleted');
2873
  } catch (e) {
2874
+ setNotesStatus(`Delete failed: ${e?.message || e}`);
2875
  }
2876
  }
2877
 
supabase_notes.sql ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Notes with folder tree + CRUD
2
+ -- Run this in Supabase SQL Editor.
3
+
4
+ create table if not exists public.note_items (
5
+ id uuid primary key default gen_random_uuid(),
6
+ user_id uuid not null references auth.users (id) on delete cascade,
7
+ parent_id uuid references public.note_items (id) on delete cascade,
8
+ kind text not null check (kind in ('folder','note')),
9
+ title text not null default '',
10
+ content text not null default '',
11
+ sort_order integer not null default 0,
12
+ created_at timestamptz not null default now(),
13
+ updated_at timestamptz not null default now()
14
+ );
15
+
16
+ create index if not exists note_items_user_id_idx on public.note_items (user_id);
17
+ create index if not exists note_items_parent_id_idx on public.note_items (parent_id);
18
+
19
+ create or replace function public.set_note_items_updated_at()
20
+ returns trigger
21
+ language plpgsql
22
+ as $$
23
+ begin
24
+ new.updated_at = now();
25
+ return new;
26
+ end;
27
+ $$;
28
+
29
+ drop trigger if exists note_items_set_updated_at on public.note_items;
30
+ create trigger note_items_set_updated_at
31
+ before update on public.note_items
32
+ for each row
33
+ execute procedure public.set_note_items_updated_at();
34
+
35
+ alter table public.note_items enable row level security;
36
+
37
+ drop policy if exists "note_items_select_own" on public.note_items;
38
+ create policy "note_items_select_own"
39
+ on public.note_items
40
+ for select
41
+ using (auth.uid() = user_id);
42
+
43
+ drop policy if exists "note_items_insert_own" on public.note_items;
44
+ create policy "note_items_insert_own"
45
+ on public.note_items
46
+ for insert
47
+ with check (auth.uid() = user_id);
48
+
49
+ drop policy if exists "note_items_update_own" on public.note_items;
50
+ create policy "note_items_update_own"
51
+ on public.note_items
52
+ for update
53
+ using (auth.uid() = user_id)
54
+ with check (auth.uid() = user_id);
55
+
56
+ drop policy if exists "note_items_delete_own" on public.note_items;
57
+ create policy "note_items_delete_own"
58
+ on public.note_items
59
+ for delete
60
+ using (auth.uid() = user_id);
61
+