Skip to content

Commit 73d2ca6

Browse files
authored
Note making app
ADD, EDIT YOUR NOTES
1 parent 20c763d commit 73d2ca6

1 file changed

Lines changed: 148 additions & 0 deletions

File tree

notes_web_app.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from http.server import BaseHTTPRequestHandler, HTTPServer
2+
import webbrowser
3+
4+
index_html = """<!doctype html>
5+
<html lang="en">
6+
<head>
7+
<meta charset="utf-8"/>
8+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
9+
<title>PARZi NOTES</title>
10+
<style>
11+
:root{--bg:#0f1724;--card:#0b1220;--muted:#9aa4b2;--accent:#7c3aed;--glass: rgba(255,255,255,0.03)}
12+
html,body{height:100%;margin:0;font-family:Inter,system-ui,Arial;background:linear-gradient(180deg,#071024 0%,#081827 100%);color:#e6eef6}
13+
.container{max-width:1100px;margin:28px auto;padding:22px}
14+
.header{display:flex;align-items:center;justify-content:space-between}
15+
.brand{display:flex;gap:14px;align-items:center}
16+
.logo{width:48px;height:48px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#2dd4bf);display:flex;align-items:center;justify-content:center;font-weight:700;color:white}
17+
.title{font-size:1.2rem;font-weight:600}
18+
.subtitle{color:var(--muted);font-size:0.85rem}
19+
.layout{display:grid;grid-template-columns:360px 1fr;gap:18px;margin-top:18px}
20+
.panel{background:var(--card);border-radius:12px;padding:14px;box-shadow:0 6px 18px rgba(2,6,23,0.6)}
21+
.search{width:100%;padding:10px;border-radius:10px;border:0;background:var(--glass);color:inherit}
22+
.notes-list{margin-top:12px;display:flex;flex-direction:column;gap:10px;max-height:68vh;overflow:auto;padding-right:6px}
23+
.note-card{padding:12px;border-radius:10px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));cursor:pointer;transition:0.2s}
24+
.note-card:hover{background:rgba(124,58,237,0.1)}
25+
.note-title{font-weight:600}
26+
.note-snippet{color:var(--muted);font-size:0.85rem;margin-top:6px}
27+
.editor{min-height:56vh;display:flex;flex-direction:column}
28+
.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
29+
.btn{background:var(--glass);border:0;padding:8px 10px;border-radius:8px;cursor:pointer;color:inherit;transition:0.2s}
30+
.btn:hover{background:rgba(124,58,237,0.15)}
31+
.btn.active{background:rgba(124,58,237,0.25)}
32+
.btn.primary{background:linear-gradient(90deg,var(--accent),#06b6d4);color:white}
33+
.title-input{width:100%;padding:10px;border-radius:8px;border:0;background:transparent;color:inherit;font-size:1.05rem;font-weight:600}
34+
.editable{flex:1;padding:12px;border-radius:10px;background:rgba(255,255,255,0.02);overflow:auto}
35+
.small{font-size:0.85rem;color:var(--muted)}
36+
.note-meta{display:flex;gap:10px;align-items:center}
37+
.actions{display:flex;gap:8px}
38+
@media (max-width:880px){.layout{grid-template-columns:1fr}.notes-list{max-height:26vh}}
39+
</style>
40+
</head>
41+
<body>
42+
<div class="container">
43+
<div class="header">
44+
<div class="brand"><div class="logo">
45+
<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24" width="24" height="24">
46+
<path d="M19 2H5c-1.1 0-2 .9-2 2v16l4-4h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H7l-2 2V4h14v10z"/>
47+
</svg></div><div><div class="title">PARZi NOTES</div><div class="subtitle">Local-first • Single-file</div></div></div>
48+
<div><button id="new-note-btn" class="btn primary">New Note</button></div>
49+
</div>
50+
<div class="layout">
51+
<div class="panel">
52+
<input id="search" class="search" type="search" placeholder="Search for notes.."/>
53+
<div id="notes-list" class="notes-list"></div>
54+
</div>
55+
<div class="panel editor">
56+
<div class="note-meta"><input id="note-title" class="title-input" type="text" placeholder="Untitled note"/><div class="actions"><button id="save-btn" class="btn primary">Save</button><button id="delete-btn" class="btn">Delete</button></div></div>
57+
<div class="toolbar">
58+
<button id="fmt-bold" class="btn">B</button>
59+
<button id="fmt-italic" class="btn">I</button>
60+
<button id="fmt-underline" class="btn">U</button>
61+
<button id="fmt-h1" class="btn">H1</button>
62+
<button id="fmt-ul" class="btn">• List</button>
63+
<button id="fmt-ol" class="btn">1. List</button>
64+
<button id="fmt-link" class="btn">Link</button>
65+
</div>
66+
<div id="editor" class="editable" contenteditable="true" role="textbox" aria-label="Note editor"></div>
67+
</div>
68+
</div>
69+
<div style="text-align:center;margin-top:14px;color:var(--muted);font-size:0.85rem;">Notes live in your browser (localStorage). No account needed.</div>
70+
</div>
71+
<script>
72+
(function(){
73+
const STORAGE_KEY='notes_v2';
74+
const notesListEl=document.getElementById('notes-list');
75+
const editorEl=document.getElementById('editor');
76+
const titleEl=document.getElementById('note-title');
77+
const searchEl=document.getElementById('search');
78+
const newBtn=document.getElementById('new-note-btn');
79+
const saveBtn=document.getElementById('save-btn');
80+
const deleteBtn=document.getElementById('delete-btn');
81+
let notes=[];
82+
let activeId=null;
83+
function nowISO(){return new Date().toISOString()}
84+
function load(){try{const raw=localStorage.getItem(STORAGE_KEY);const parsed=raw?JSON.parse(raw):[];notes=Array.isArray(parsed)?parsed:[]}catch(e){notes=[]}}
85+
function saveAll(){try{localStorage.setItem(STORAGE_KEY,JSON.stringify(notes))}catch(e){console.error('save failed',e)}}
86+
function stripHtml(html){const d=document.createElement('div');d.innerHTML=html||'';return d.textContent||d.innerText||''}
87+
function snippet(html){const txt=stripHtml(html).trim();return txt.length>120?txt.slice(0,120)+'...':(txt||'(empty)')}
88+
function renderNotesList(filter){if(!notesListEl) return;notesListEl.innerHTML='';const f=String(filter||'').toLowerCase();const sorted=notes.slice().sort((a,b)=>new Date(b.updated_at||b.created_at)-new Date(a.updated_at||a.created_at));const filtered=sorted.filter(n=>((n.title||'').toLowerCase().includes(f))||((stripHtml(n.content)||'').toLowerCase().includes(f)));if(filtered.length===0){notesListEl.innerHTML='<div class="small" style="padding:8px;color:var(--muted)">No saved notes yet</div>';return}for(const n of filtered){const card=document.createElement('div');card.className='note-card';if(n.id===activeId) card.className+=' active-note';card.dataset.id=n.id;const t=document.createElement('div');t.className='note-title';t.innerText=n.title||'Untitled';const s=document.createElement('div');s.className='note-snippet';s.innerText=snippet(n.content);const meta=document.createElement('div');meta.className='small';meta.innerText=new Date(n.updated_at||n.created_at).toLocaleString();card.appendChild(t);card.appendChild(s);card.appendChild(meta);card.addEventListener('click',()=>openNote(n.id));notesListEl.appendChild(card)}}
89+
function highlightActive(){const cards=document.querySelectorAll('.note-card');cards.forEach(c=>{c.style.outline=(c.dataset.id===activeId)?'2px solid rgba(124,58,237,0.18)':'none'})}
90+
function createNoteWithContent(title,content){const id='n_'+Math.random().toString(36).slice(2,9);const now=nowISO();const n={id,title:title||'',content:content||'',created_at:now,updated_at:now};notes.push(n);saveAll();renderNotesList(searchEl?searchEl.value.toLowerCase():'');openNote(id);return n}
91+
function newNote(){createNoteWithContent('','');if(titleEl) titleEl.focus()}
92+
function openNote(id){const n=notes.find(x=>x.id===id);if(!n) return;activeId=id;if(titleEl) titleEl.value=n.title||'';if(editorEl) editorEl.innerHTML=n.content||'';highlightActive()}
93+
function saveNote(){const title=(titleEl&&titleEl.value!==undefined)?titleEl.value.trim():'';const content=(editorEl&&editorEl.innerHTML!==undefined)?editorEl.innerHTML:'';if(!activeId){const created=createNoteWithContent(title,content);created.title=title;created.content=content;created.updated_at=nowISO();saveAll();renderNotesList(searchEl?searchEl.value.toLowerCase():'');activeId=created.id;flash('Saved');return}const n=notes.find(x=>x.id===activeId);if(!n){const created=createNoteWithContent(title,content);created.updated_at=nowISO();saveAll();renderNotesList(searchEl?searchEl.value.toLowerCase():'');activeId=created.id;flash('Saved');return}n.title=title;n.content=content;n.updated_at=nowISO();saveAll();renderNotesList(searchEl?searchEl.value.toLowerCase():'');flash('Saved')}
94+
function deleteNote(){if(!activeId) return;const ok=confirm('Delete this note?');if(!ok) return;notes=notes.filter(x=>x.id!==activeId);saveAll();activeId=null;if(titleEl) titleEl.value='';if(editorEl) editorEl.innerHTML='';renderNotesList(searchEl?searchEl.value.toLowerCase():'');flash('Deleted')}
95+
function flash(msg){const el=document.createElement('div');el.innerText=msg;el.style.position='fixed';el.style.right='18px';el.style.bottom='18px';el.style.padding='10px 12px';el.style.borderRadius='8px';el.style.background='rgba(0,0,0,0.6)';el.style.color='white';el.style.zIndex='9999';document.body.appendChild(el);setTimeout(()=>{el.style.transition='opacity 300ms';el.style.opacity='0'},1200);setTimeout(()=>el.remove(),1600)}
96+
function format(cmd,val){try{document.execCommand(cmd,false,val)}catch(e){console.warn('format failed',e)}if(editorEl) editorEl.focus()}
97+
function insertLink(){const url=prompt('Enter a URL (include https://)');if(!url) return;format('createLink',url)}
98+
newBtn.addEventListener('click',()=>{newNote();if(titleEl) titleEl.focus()});if(saveBtn) saveBtn.addEventListener('click',saveNote);if(deleteBtn) deleteBtn.addEventListener('click',deleteNote);
99+
[['fmt-bold','bold'],['fmt-italic','italic'],['fmt-underline','underline']].forEach(([id,cmd])=>{
100+
const el=document.getElementById(id);
101+
if(el) el.addEventListener('click',()=>{
102+
format(cmd);
103+
el.classList.toggle('active');
104+
})
105+
});
106+
const h1=document.getElementById('fmt-h1');if(h1) h1.addEventListener('click',()=>{format('formatBlock','<h1>');h1.classList.toggle('active')});
107+
const fu=document.getElementById('fmt-ul');if(fu) fu.addEventListener('click',()=>{format('insertUnorderedList');fu.classList.toggle('active')});
108+
const fo=document.getElementById('fmt-ol');if(fo) fo.addEventListener('click',()=>{format('insertOrderedList');fo.classList.toggle('active')});
109+
const flink=document.getElementById('fmt-link');if(flink) flink.addEventListener('click',()=>{insertLink();flink.classList.toggle('active')});
110+
let saveTimer=null;
111+
function scheduleAutoSave(){if(saveTimer) clearTimeout(saveTimer);saveTimer=setTimeout(()=>{saveNote();saveTimer=null},900)}
112+
if(editorEl) editorEl.addEventListener('input',scheduleAutoSave);
113+
if(titleEl) titleEl.addEventListener('input',scheduleAutoSave);
114+
if(searchEl) searchEl.addEventListener('input',e=>renderNotesList(String(e.target.value||'').toLowerCase()));
115+
load();
116+
renderNotesList();
117+
if(notes.length){const last=notes.slice().sort((a,b)=>new Date(b.updated_at||b.created_at)-new Date(a.updated_at||a.created_at))[0];if(last) openNote(last.id)}
118+
window.addEventListener('keydown',e=>{const mod=e.ctrlKey||e.metaKey;if(mod&&e.key&&e.key.toLowerCase()==='s'){e.preventDefault();saveNote()}if(mod&&e.key&&e.key.toLowerCase()==='n'){e.preventDefault();newNote()}});
119+
window.NOTES_APP={get notes(){return notes},saveAll(){saveAll()}}
120+
})();
121+
</script>
122+
</body>
123+
</html>
124+
"""
125+
126+
class Handler(BaseHTTPRequestHandler):
127+
def do_GET(self):
128+
if self.path in ('/', '/index.html'):
129+
self.send_response(200)
130+
self.send_header('Content-type', 'text/html; charset=utf-8')
131+
self.end_headers()
132+
self.wfile.write(index_html.encode('utf-8'))
133+
else:
134+
self.send_response(404)
135+
self.end_headers()
136+
137+
if __name__ == '__main__':
138+
port = 8000
139+
try:
140+
server = HTTPServer(('0.0.0.0', port), Handler)
141+
print(f'Serving at http://127.0.0.1:{port}')
142+
try:
143+
webbrowser.open(f'http://127.0.0.1:{port}')
144+
except Exception:
145+
pass
146+
server.serve_forever()
147+
except OSError as e:
148+
print('Port in use or permission denied:', e)

0 commit comments

Comments
 (0)