Skip to content
Gosia edited this page Oct 16, 2025 · 1 revision

Welcome to the site wiki! // AureaFoldApp.jsx // Prototype React single-file app (Tailwind + Recharts) // Natural & Zen vibe. Privacy-first: data stored to localStorage + IndexedDB for audio blobs.

import React, { useState, useEffect, useRef } from "react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";

// Simple IndexedDB helper for audio blobs const IDB_DB = 'aureafold-db'; const IDB_STORE = 'audioBlobs'; function openIdb(){ return new Promise((res, rej) => { const rq = indexedDB.open(IDB_DB, 1); rq.onupgradeneeded = () => rq.result.createObjectStore(IDB_STORE); rq.onsuccess = () => res(rq.result); rq.onerror = () => rej(rq.error); }); } async function idbPut(key, blob){ const db = await openIdb(); return new Promise((res, rej) => { const tx = db.transaction(IDB_STORE, 'readwrite'); tx.objectStore(IDB_STORE).put(blob, key); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } async function idbGet(key){ const db = await openIdb(); return new Promise((res, rej) => { const tx = db.transaction(IDB_STORE, 'readonly'); const rq = tx.objectStore(IDB_STORE).get(key); rq.onsuccess = () => res(rq.result); rq.onerror = () => rej(rq.error); }); } async function idbDelete(key){ const db = await openIdb(); return new Promise((res, rej) => { const tx = db.transaction(IDB_STORE, 'readwrite'); tx.objectStore(IDB_STORE).delete(key); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); }

export default function AureaFoldApp() { const STORAGE_KEY = "aureafold.entries.v1"; const SETTINGS_KEY = "aureafold.settings.v1";

const [entries, setEntries] = useState([]); const [form, setForm] = useState({ date: new Date().toISOString().slice(0,16), mood: 6, energy: 6, tag: "", body: "", thoughts: "", beliefs: "", practice: "", notes: "", audioKey: null, transcript: "", });

const [settings, setSettings] = useState({ reminders: { morning: "08:00", midday: "13:00", evening: "20:00" }, notifyEnabled: true, });

const prompts = [ "What intention do I hold for my spine and breath today?", "Name one small kindness you can offer yourself right now.", "Notice one belief that feels softer today compared to last week.", "Where does your body ask for more tenderness this morning?", "Describe a moment today when you felt present — what changed?", "What micro-practice will support your openness this afternoon?", "What did today teach your heart about trust?", ]; const [promptIndex, setPromptIndex] = useState(() => Math.floor(Math.random()*prompts.length));

// audio recording + speech recognition const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const [recording, setRecording] = useState(false); const [recordingBlobUrl, setRecordingBlobUrl] = useState(null); const recognitionRef = useRef(null); const [recognitionAvailable, setRecognitionAvailable] = useState(false);

useEffect(() => { const raw = localStorage.getItem(STORAGE_KEY); if (raw) setEntries(JSON.parse(raw)); const rawSet = localStorage.getItem(SETTINGS_KEY); if (rawSet) setSettings(JSON.parse(rawSet));

// feature-detect Web Speech API (SpeechRecognition)
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || null;
if (SpeechRecognition) setRecognitionAvailable(true);

}, []);

useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(entries)); }, [entries]); useEffect(() => { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); }, [settings]);

// rotate prompt at midnight useEffect(() => { const now = new Date(); const msUntilMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1) - now; const t = setTimeout(() => setPromptIndex(i => (i+1)%prompts.length), msUntilMidnight+1000); return () => clearTimeout(t); }, [promptIndex]);

// reminders polling useEffect(() => { let interval = null; if (settings.notifyEnabled && typeof Notification !== 'undefined') { if (Notification.permission !== 'granted') Notification.requestPermission(); } interval = setInterval(() => { const now = new Date(); const hm = now.toTimeString().slice(0,5); Object.entries(settings.reminders).forEach(([key, time]) => { const lastFiredKey = lastFired_${key}; const last = localStorage.getItem(lastFiredKey) || ''; if (hm === time && (Date.now() - (Number(last)||0) > 60000)) { if (Notification.permission === 'granted') { new Notification(AureaFold — ${key} reminder, { body: "Pause, breathe, and log a short pulse." }); } localStorage.setItem(lastFiredKey, String(Date.now())); setShowReminderCue({ key, time, ts: Date.now() }); } }); }, 10000); return () => clearInterval(interval); }, [settings]);

const [showReminderCue, setShowReminderCue] = useState(null);

function handleChange(e){ const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); }

async function startRecording(){ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { alert('Audio recording not supported in this browser.'); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mr = new MediaRecorder(stream); mediaRecorderRef.current = mr; audioChunksRef.current = []; mr.ondataavailable = e => audioChunksRef.current.push(e.data); mr.onstop = async () => { const blob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); // store blob in IndexedDB with key const key = audio_${Date.now()}; try { await idbPut(key, blob); setForm(prev => ({ ...prev, audioKey: key })); // create object URL for playback UI const url = URL.createObjectURL(blob); setRecordingBlobUrl(url); } catch(err){ console.error('IndexedDB save failed', err); alert('Failed to save audio locally.'); } }; mr.start(); setRecording(true);

  // Start Web Speech API recognition in parallel if available
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || null;
  if (SpeechRecognition) {
    try {
      const recog = new SpeechRecognition();
      recog.lang = 'en-US';
      recog.interimResults = true;
      let interim = '';
      let final = '';
      recog.onresult = (ev) => {
        interim = '';
        for (let i=ev.resultIndex;i<ev.results.length;i++){
          const r = ev.results[i];
          if (r.isFinal) final += r[0].transcript + ' ';
          else interim += r[0].transcript;
        }
        setForm(prev => ({ ...prev, transcript: final + interim }));
      };
      recog.onerror = (e) => { console.warn('Speech recognition error', e); };
      recog.start();
      recognitionRef.current = recog;
    } catch(err){ console.warn('Speech recognition start failed', err); }
  }

} catch (err) {
  alert('Could not start recording: ' + err.message);
}

}

function stopRecording(){ const mr = mediaRecorderRef.current; if (mr) { mr.stop(); mr.stream?.getTracks().forEach(t => t.stop()); } setRecording(false); // stop recognition try { recognitionRef.current?.stop(); recognitionRef.current = null; } catch(e){} }

async function playAudioFromKey(key){ const blob = await idbGet(key); if (!blob) return alert('Audio not found'); const url = URL.createObjectURL(blob); const a = new Audio(url); a.play(); }

async function downloadAudioFromKey(key){ const blob = await idbGet(key); if (!blob) return alert('Audio not found'); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = ${key}.webm; a.click(); URL.revokeObjectURL(url); }

async function addEntry(e){ e?.preventDefault(); const entry = { id: Date.now(), date: form.date, mood: Number(form.mood), energy: Number(form.energy), tag: form.tag.trim(), body: form.body.trim(), thoughts: form.thoughts.trim(), beliefs: form.beliefs.trim(), practice: form.practice.trim(), notes: form.notes.trim(), audioKey: form.audioKey || null, transcript: form.transcript || '', beliefShift: detectBeliefShift((form.beliefs||'') + ' ' + (form.thoughts||'')), }; setEntries(prev => [entry, ...prev]); setForm(prev => ({ ...prev, mood:6, energy:6, tag:'', body:'', thoughts:'', beliefs:'', practice:'', notes:'', audioKey:null, transcript:'' })); setRecordingBlobUrl(null); }

async function deleteEntry(id){ const en = entries.find(x=>x.id===id); if (!en) return; if (en.audioKey) await idbDelete(en.audioKey); setEntries(prev => prev.filter(x=>x.id!==id)); }

function exportCSV(){ const header = ["id","date","mood","energy","tag","body","thoughts","beliefs","practice","notes","audioKey","transcript"]; const rows = entries.map(en => header.map(h => JSON.stringify(en[h] ?? "")).join(",")); const csv = [header.join(","), ...rows].join(" "); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'aureafold-entries.csv'; a.click(); URL.revokeObjectURL(url); }

function exportEncrypted(){ const json = JSON.stringify(entries); const pass = prompt('Enter a passphrase to encrypt your backup (you will need it to decrypt)'); if (!pass) return alert('Export canceled'); (async ()=>{ const enc = new TextEncoder().encode(pass); const keyMaterial = await window.crypto.subtle.importKey('raw', enc, {name:'PBKDF2'}, false, ['deriveKey']); const salt = window.crypto.getRandomValues(new Uint8Array(16)); const key = await window.crypto.subtle.deriveKey({name:'PBKDF2', salt, iterations: 100000, hash: 'SHA-256'}, keyMaterial, {name:'AES-GCM', length:256}, true, ['encrypt']); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const cipher = await window.crypto.subtle.encrypt({name:'AES-GCM', iv}, key, new TextEncoder().encode(json)); const payload = { salt: Array.from(salt), iv: Array.from(iv), cipher: Array.from(new Uint8Array(cipher)) }; const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='aureafold-backup.enc.json'; a.click(); URL.revokeObjectURL(url); })(); }

function chartData(days = 30){ const msDay = 2460601000; const now = Date.now(); const map = {}; entries.forEach(en => { const d = new Date(en.date); const key = d.toISOString().slice(0,10); if (!map[key]) map[key] = { sum:0, count:0 }; map[key].sum += en.mood; map[key].count += 1; }); const list = []; for (let i = days-1; i >= 0; i--) { const d = new Date(now - imsDay); const key = d.toISOString().slice(0,10); if (map[key]) list.push({ date: key, mood: +(map[key].sum/map[key].count).toFixed(2) }); else list.push({ date: key, mood: null }); } return list; }

const practices = [ { id:'breath1', title:'1-min Soothing Breath', desc:'4 in — hold 2 — 6 out. Repeat for 1 minute.' }, { id:'ground1', title:'Grounding Root', desc:'Stand, press feet into floor, imagine roots going deep for 2 minutes.' }, { id:'yoga1', title:'Neck & Heart Opener', desc:'Seated gentle neck rolls then heart-opening stretch for 2 minutes.' }, ];

const musicPicks = [ { title:'432Hz Healing Soundscape', link:'https://www.youtube.com/results?search_query=432hz+meditation' }, { title:'Gentle Ambient Focus', link:'https://www.youtube.com/results?search_query=ambient+focus+music' }, { title:'Solfeggio 528Hz', link:'https://www.youtube.com/results?search_query=528hz+solfeggio' }, ];

function detectBeliefShift(text){ const patterns = [/I no longer/i, /I used to/i, /I now believe/i, /I don't believe/i, /I can't/i, /I can/i, /I am not/i, /I am now/i]; for (const p of patterns) if (p.test(text)) return true; return false; }

const recent = entries.slice(0,10);

return (

AureaFold

Map your inner rebirth — daily, tender, private.

    <main className="grid grid-cols-1 lg:grid-cols-2 gap-6">
      <section className="p-4 bg-amber-50 rounded"> 
        <form onSubmit={addEntry} className="space-y-3">
          <div className="flex gap-2">
            <input type="datetime-local" name="date" value={form.date} onChange={handleChange} className="flex-1 rounded border p-2" />
          </div>

          <div className="flex items-center gap-4">
            <div className="flex-1">
              <label className="block text-xs">Mood</label>
              <input type="range" min="1" max="10" name="mood" value={form.mood} onChange={handleChange} className="w-full" />
              <div className="text-sm">{form.mood}</div>
            </div>
            <div className="flex-1">
              <label className="block text-xs">Energy</label>
              <input type="range" min="1" max="10" name="energy" value={form.energy} onChange={handleChange} className="w-full" />
              <div className="text-sm">{form.energy}</div>
            </div>
          </div>

          <div>
            <label className="block text-xs">Quick Tag</label>
            <input name="tag" value={form.tag} onChange={handleChange} className="w-full rounded border p-2" placeholder="meditation, breakthrough, trigger" />
          </div>

          <div>
            <label className="block text-xs">Body Sensations (short)</label>
            <textarea name="body" value={form.body} onChange={handleChange} rows={2} className="w-full rounded border p-2" placeholder="Warmth in chest, tingling crown..." />
          </div>

          <div>
            <label className="block text-xs">Thoughts / Journal</label>
            <textarea name="thoughts" value={form.thoughts} onChange={handleChange} rows={3} className="w-full rounded border p-2" placeholder="Capture insights or repeating thoughts..." />
          </div>

          <div>
            <label className="block text-xs">Beliefs / Shifts</label>
            <input name="beliefs" value={form.beliefs} onChange={handleChange} className="w-full rounded border p-2" placeholder="I no longer... / I now believe..." />
          </div>

          <div className="flex gap-2">
            <button type="button" onClick={recording?stopRecording:startRecording} className="px-3 py-2 rounded bg-green-600 text-white text-sm">{recording? 'Stop' : 'Record'} Audio</button>
            {recordingBlobUrl && <button type="button" onClick={()=>{ downloadAudioFromKey(form.audioKey); }} className="px-3 py-2 rounded border text-sm">Download</button>}
            <button type="submit" className="ml-auto px-3 py-2 rounded bg-indigo-600 text-white text-sm">Save Entry</button>
          </div>

          <div className="text-xs text-slate-600">Prompt: <span className="italic">{prompts[promptIndex]}</span> — <button type="button" onClick={()=>setPromptIndex((promptIndex+1)%prompts.length)} className="text-xs underline">Next prompt</button></div>
        </form>

        <div className="mt-4 p-3 bg-white rounded">
          <h4 className="font-semibold text-sm">Quick Practices</h4>
          <div className="space-y-2 mt-2">
            {practices.map(p => (
              <div key={p.id} className="p-2 rounded border">
                <div className="flex justify-between items-center">
                  <div>
                    <div className="font-medium text-sm">{p.title}</div>
                    <div className="text-xs text-slate-600">{p.desc}</div>
                  </div>
                  <button onClick={()=>{ alert(p.desc); }} className="text-sm px-2 py-1 border rounded">Do now</button>
                </div>
              </div>
            ))}
          </div>

          <div className="mt-3">
            <h4 className="font-semibold text-sm">Music & Frequencies</h4>
            <ul className="mt-2 space-y-1 text-xs">
              {musicPicks.map(m => (
                <li key={m.title}><a className="underline" href={m.link} target="_blank" rel="noreferrer">{m.title}</a></li>
              ))}
            </ul>
          </div>
        </div>
      </section>

      <aside className="p-4 bg-amber-50 rounded">
        <div className="mb-3">
          <h3 className="font-semibold">Recent Entries</h3>
          <div className="mt-2 space-y-2 max-h-64 overflow-auto">
            {recent.length===0 && <div className="text-sm text-slate-500">No entries yet — begin with a short note or record.</div>}
            {recent.map(en => (
              <div key={en.id} className="p-2 bg-white rounded shadow-sm">
                <div className="text-xs text-slate-500">{new Date(en.date).toLocaleString()}</div>
                <div className="font-medium text-sm">Mood {en.mood} • Energy {en.energy} • {en.tag}</div>
                <div className="text-xs mt-1">{en.body || en.thoughts || en.beliefs || en.notes}</div>
                <div className="mt-2 flex gap-2">
                  {en.audioKey && <button onClick={()=>playAudioFromKey(en.audioKey)} className="text-xs px-2 py-1 border rounded">Play</button>}
                  {en.audioKey && <button onClick={()=>downloadAudioFromKey(en.audioKey)} className="text-xs px-2 py-1 border rounded">Download</button>}
                  <button onClick={()=>{ navigator.clipboard?.writeText(JSON.stringify(en)); alert('Copied entry JSON'); }} className="text-xs px-2 py-1 border rounded">Copy</button>
                  <button onClick={()=>deleteEntry(en.id)} className="text-xs px-2 py-1 border rounded">Delete</button>
                </div>
                {en.transcript && <div className="mt-2 text-xs bg-amber-50 p-2 rounded">Transcript: {en.transcript}</div>}
              </div>
            ))}
          </div>
        </div>

        <div className="mb-3 bg-white p-3 rounded">
          <h3 className="font-semibold text-sm">Mood Trend</h3>
          <div style={{ width:'100%', height:180 }} className="mt-2">
            <ResponsiveContainer>
              <LineChart data={chartData(30)}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="date" />
                <YAxis domain={[1,10]} />
                <Tooltip />
                <Line type="monotone" dataKey="mood" stroke="#b7791f" strokeWidth={2} connectNulls />
              </LineChart>
            </ResponsiveContainer>
          </div>
        </div>

        <div className="p-3 bg-white rounded">
          <h3 className="font-semibold text-sm">Settings & Export</h3>
          <div className="mt-2 text-xs">
            <label className="block">Morning reminder <input type="time" value={settings.reminders.morning} onChange={(e)=>setSettings(s=>({...s, reminders:{...s.reminders, morning:e.target.value}}))} className="ml-2" /></label>
            <label className="block">Midday reminder <input type="time" value={settings.reminders.midday} onChange={(e)=>setSettings(s=>({...s, reminders:{...s.reminders, midday:e.target.value}}))} className="ml-2" /></label>
            <label className="block">Evening reminder <input type="time" value={settings.reminders.evening} onChange={(e)=>setSettings(s=>({...s, reminders:{...s.reminders, evening:e.target.value}}))} className="ml-2" /></label>
            <label className="block mt-2">Enable notifications <input type="checkbox" checked={settings.notifyEnabled} onChange={(e)=>setSettings(s=>({...s, notifyEnabled: e.target.checked}))} className="ml-2" /></label>
            <div className="mt-2 flex gap-2">
              <button onClick={exportCSV} className="px-2 py-1 rounded border text-xs">Export CSV</button>
              <button onClick={exportEncrypted} className="px-2 py-1 rounded border text-xs">Encrypted Backup</button>
            </div>
          </div>
        </div>

      </aside>
    </main>

    <footer className="mt-6 text-sm text-slate-500">
      <div>AureaFold prototype — Natural & Zen vibe. Data stays in your browser by default. Audio stored in your browser's IndexedDB for reliability.</div>
      {showReminderCue && <div className="mt-2 p-2 bg-amber-100 rounded text-sm">Reminder: {showReminderCue.key} — {showReminderCue.time}</div>}
    </footer>
  </div>
</div>

); }

Clone this wiki locally