Skip to content
Open
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
78 changes: 78 additions & 0 deletions assets/js/platform-v3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
(function(){
const KEYS={
notes:'360_quick_notes_v1',
pomodoro:'360_pomodoro_minutes',
focus:'360_focus_mode'
};
const ROUTES=[
['Home','/'],['AI','/ai'],['Weather','/weather'],['Translator','/translator'],['Stocks','/stocks'],['Chat','/chat'],['News','/news'],['Apps','/apps'],['Games','/games'],['Settings','/settings.html']
];

function injectUI(){
if(document.getElementById('v3Fab')) return;
const style=document.createElement('style');
style.textContent=`
#v3Fab{position:fixed;right:14px;bottom:14px;z-index:1200;border:none;border-radius:999px;padding:10px 14px;background:linear-gradient(120deg,var(--a),var(--a2));font-weight:700;cursor:pointer}
#v3Panel{position:fixed;right:14px;bottom:58px;z-index:1200;width:min(360px,92vw);background:rgba(15,23,42,.9);color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:12px;padding:10px;display:none;backdrop-filter:blur(8px)}
#v3Panel.open{display:block}.v3row{display:flex;gap:6px;margin-bottom:8px}.v3row>*{flex:1}
.v3input{width:100%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff}
.v3btn{padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.12);color:#fff;cursor:pointer}
#v3Cmd{position:fixed;inset:0;background:rgba(2,6,23,.7);z-index:1300;display:none;align-items:flex-start;justify-content:center;padding-top:12vh}
#v3Cmd.open{display:flex}#v3CmdBox{width:min(680px,92vw);background:#0f172a;border:1px solid rgba(255,255,255,.15);border-radius:12px;padding:10px}
#v3CmdList button{display:block;width:100%;text-align:left;margin-top:6px}
body.v3-focus .sidebar, body.v3-focus .auth-top-right, body.v3-focus .settings-panel{display:none!important}
`;
document.head.appendChild(style);

const fab=document.createElement('button'); fab.id='v3Fab'; fab.textContent='V3 ⚡';
const panel=document.createElement('section'); panel.id='v3Panel'; panel.innerHTML=`
<div style="font-size:12px;opacity:.8;margin-bottom:8px;">Quick Hub (Frontend-only)</div>
<div class="v3row"><button class="v3btn" id="v3FocusBtn">Toggle Focus</button><button class="v3btn" id="v3CmdBtn">Command Palette</button></div>
<div class="v3row"><input class="v3input" id="v3Pomodoro" type="number" min="5" max="120" placeholder="Pomodoro minutes"/><button class="v3btn" id="v3PomodoroStart">Start</button></div>
<textarea class="v3input" id="v3Notes" rows="4" placeholder="Quick notes (local)"></textarea>
<div class="v3row"><button class="v3btn" id="v3SaveNotes">Save Notes</button><button class="v3btn" id="v3ExportNotes">Export .txt</button></div>
`;

const cmd=document.createElement('div'); cmd.id='v3Cmd'; cmd.innerHTML=`<div id="v3CmdBox"><input class="v3input" id="v3CmdSearch" placeholder="Type to jump pages... (esc to close)"/><div id="v3CmdList"></div></div>`;

document.body.append(fab,panel,cmd);

const notesEl=panel.querySelector('#v3Notes');
const pomoEl=panel.querySelector('#v3Pomodoro');
notesEl.value=localStorage.getItem(KEYS.notes)||'';
pomoEl.value=localStorage.getItem(KEYS.pomodoro)||'25';
if(localStorage.getItem(KEYS.focus)==='true') document.body.classList.add('v3-focus');

fab.onclick=()=>panel.classList.toggle('open');
panel.querySelector('#v3FocusBtn').onclick=()=>{document.body.classList.toggle('v3-focus');localStorage.setItem(KEYS.focus,String(document.body.classList.contains('v3-focus')))};
panel.querySelector('#v3SaveNotes').onclick=()=>localStorage.setItem(KEYS.notes,notesEl.value);
panel.querySelector('#v3ExportNotes').onclick=()=>{const b=new Blob([notesEl.value],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='360-notes.txt';a.click();URL.revokeObjectURL(a.href)};
panel.querySelector('#v3PomodoroStart').onclick=()=>{
const mins=Math.max(5,Math.min(120,Number(pomoEl.value)||25));
localStorage.setItem(KEYS.pomodoro,String(mins));
const end=Date.now()+mins*60000;
const tick=()=>{
const left=Math.max(0,Math.ceil((end-Date.now())/1000));
fab.textContent=left>0?`⏱ ${Math.floor(left/60)}:${String(left%60).padStart(2,'0')}`:'Done ✅';
if(left>0) requestAnimationFrame(tick); else setTimeout(()=>fab.textContent='V3 ⚡',4000);
};tick();
};

const cmdList=cmd.querySelector('#v3CmdList');
function renderCmd(q=''){
const qq=q.toLowerCase();
cmdList.innerHTML='';
ROUTES.filter(r=>r[0].toLowerCase().includes(qq)).forEach(([n,u])=>{const b=document.createElement('button');b.className='v3btn';b.textContent=`Go to ${n}`;b.onclick=()=>location.href=u;cmdList.appendChild(b);});
}
renderCmd();
panel.querySelector('#v3CmdBtn').onclick=()=>{cmd.classList.add('open');cmd.querySelector('#v3CmdSearch').focus();};
cmd.querySelector('#v3CmdSearch').oninput=(e)=>renderCmd(e.target.value);
cmd.addEventListener('click',e=>{if(e.target===cmd) cmd.classList.remove('open');});
document.addEventListener('keydown',e=>{
if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='k'){e.preventDefault();cmd.classList.add('open');cmd.querySelector('#v3CmdSearch').focus();}
if(e.key==='Escape') cmd.classList.remove('open');
});
}

document.addEventListener('DOMContentLoaded', injectUI);
})();
196 changes: 196 additions & 0 deletions assets/js/widgets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
(function () {
const STORAGE_KEY = '360_widgets_v1';
const TYPES = {
weather: { label: 'Weather' },
time: { label: 'Time' },
note: { label: 'Note' }
};

function loadWidgets() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; }
}
function saveWidgets(w) { localStorage.setItem(STORAGE_KEY, JSON.stringify(w)); }
function uid() { return `w_${Date.now()}_${Math.random().toString(36).slice(2,8)}`; }
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));

async function getWeather(lat, lon, unit) {
const tempUnit = unit === 'F' ? 'fahrenheit' : 'celsius';
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m&temperature_unit=${tempUnit}`;
const res = await fetch(url);
const data = await res.json();
return data?.current?.temperature_2m;
}

function applyWidgetStyles(card, header, body, w) {
const opacity = clamp(Number(w.opacity ?? 0.85), 0.2, 1);
card.style.background = w.bgColor || `rgba(15,23,42,${opacity})`;
card.style.color = w.textColor || '#ffffff';
card.style.borderRadius = `${clamp(Number(w.radius ?? 12), 0, 48)}px`;
card.style.borderColor = w.borderColor || 'var(--br)';
body.style.fontSize = `${clamp(Number(w.fontSize ?? 18), 10, 48)}px`;
if ((w.shape || 'rounded') === 'pill') card.style.borderRadius = '999px';
if ((w.shape || 'rounded') === 'square') card.style.borderRadius = '0px';
header.style.background = w.headerColor || 'rgba(59,130,246,.35)';
}

function startIndexMode() {
const host = document.getElementById('widgetBoard');
if (!host) return;
let widgets = loadWidgets();

function render() {
host.innerHTML = '';
widgets.forEach(w => {
const card = document.createElement('section');
card.className = 'home-widget';
card.dataset.id = w.id;
card.style.left = (w.x || 20) + 'px';
card.style.top = (w.y || 20) + 'px';
card.style.width = `${clamp(Number(w.width || 220), 140, 600)}px`;
card.style.height = `${clamp(Number(w.height || 130), 80, 500)}px`;

const header = document.createElement('div');
header.className = 'home-widget-header';
header.textContent = w.title || TYPES[w.type]?.label || 'Widget';

const body = document.createElement('div');
body.className = 'home-widget-body';
card.append(header, body);
applyWidgetStyles(card, header, body, w);
host.appendChild(card);

if (w.type === 'time') {
const locale = w.locale || undefined;
const tz = w.timezone || undefined;
const f = () => { body.textContent = new Date().toLocaleTimeString(locale, { timeZone: tz, hour: '2-digit', minute: '2-digit', second: '2-digit' }); };
f();
setInterval(f, 1000);
} else if (w.type === 'note') {
body.textContent = w.text || 'Empty note';
} else if (w.type === 'weather') {
body.textContent = 'Loading weather…';
const unit = w.unit || 'C';
const setFallback = () => body.textContent = 'Weather unavailable';
if (!navigator.geolocation) setFallback();
else navigator.geolocation.getCurrentPosition(async pos => {
try {
const t = await getWeather(pos.coords.latitude, pos.coords.longitude, unit);
body.textContent = t == null ? 'Weather unavailable' : `${Math.round(t)}°${unit}`;
} catch { setFallback(); }
}, setFallback);
}

makeDraggable(card, w);
});
}

function makeDraggable(el, widget) {
const header = el.querySelector('.home-widget-header');
let sx=0, sy=0, ox=0, oy=0, dragging=false;
header.addEventListener('pointerdown', e => {
dragging = true;
sx = e.clientX; sy = e.clientY;
ox = widget.x || 20; oy = widget.y || 20;
el.setPointerCapture(e.pointerId);
});
header.addEventListener('pointermove', e => {
if (!dragging) return;
widget.x = Math.max(0, ox + e.clientX - sx);
widget.y = Math.max(0, oy + e.clientY - sy);
el.style.left = widget.x + 'px';
el.style.top = widget.y + 'px';
});
header.addEventListener('pointerup', () => {
dragging = false;
saveWidgets(widgets);
});
}

render();
}

function startSettingsMode() {
const list = document.getElementById('widgetList');
if (!list) return;
const form = document.getElementById('widgetForm');
const fields = {
type: document.getElementById('widgetType'),
title: document.getElementById('widgetTitle'),
width: document.getElementById('widgetWidth'),
height: document.getElementById('widgetHeight'),
text: document.getElementById('widgetText'),
unit: document.getElementById('widgetUnit'),
timezone: document.getElementById('widgetTimezone'),
locale: document.getElementById('widgetLocale'),
shape: document.getElementById('widgetShape'),
bgColor: document.getElementById('widgetBgColor'),
headerColor: document.getElementById('widgetHeaderColor'),
textColor: document.getElementById('widgetTextColor'),
borderColor: document.getElementById('widgetBorderColor'),
radius: document.getElementById('widgetRadius'),
fontSize: document.getElementById('widgetFontSize'),
opacity: document.getElementById('widgetOpacity')
};
let widgets = loadWidgets();

function refresh() {
list.innerHTML = widgets.map(w => `
<div class="st-row">
<div>
<div class="st-row-label">${w.title || w.type}</div>
<div class="st-row-sub">${w.type} • ${w.width}x${w.height} • ${(w.shape||'rounded')}</div>
</div>
<div class="st-row-right">
<button class="st-btn" data-act="dup" data-id="${w.id}">Duplicate</button>
<button class="st-btn" data-act="del" data-id="${w.id}">Delete</button>
</div>
</div>`).join('') || '<div class="st-row-sub">No widgets yet.</div>';
saveWidgets(widgets);
}

list.addEventListener('click', e => {
const btn = e.target.closest('button[data-act]');
if (!btn) return;
const { act, id } = btn.dataset;
if (act === 'del') widgets = widgets.filter(w => w.id !== id);
if (act === 'dup') {
const src = widgets.find(w => w.id === id);
if (src) widgets.push({ ...src, id: uid(), title: `${src.title || src.type} Copy`, x: (src.x || 20) + 20, y: (src.y || 20) + 20 });
}
refresh();
});

form.addEventListener('submit', e => {
e.preventDefault();
widgets.push({
id: uid(),
type: fields.type.value,
title: fields.title.value.trim() || TYPES[fields.type.value].label,
width: clamp(Number(fields.width.value) || 220, 140, 600),
height: clamp(Number(fields.height.value) || 130, 80, 500),
text: fields.text.value.trim(),
unit: fields.unit.value,
timezone: fields.timezone.value.trim(),
locale: fields.locale.value.trim(),
shape: fields.shape.value,
bgColor: fields.bgColor.value,
headerColor: fields.headerColor.value,
textColor: fields.textColor.value,
borderColor: fields.borderColor.value,
radius: clamp(Number(fields.radius.value) || 12, 0, 48),
fontSize: clamp(Number(fields.fontSize.value) || 18, 10, 48),
opacity: clamp(Number(fields.opacity.value) || 0.85, 0.2, 1),
x: 20,
y: 20
});
form.reset();
refresh();
});
refresh();
}

document.addEventListener('DOMContentLoaded', () => {
startIndexMode();
startSettingsMode();
});
})();
11 changes: 11 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@
}
.bob-track.on { background: var(--a); }
.bob-track.on::before { transform: translateX(16px); }
#widgetBoard { position: fixed; inset: 0; pointer-events: none; z-index: 5; }
.home-widget { position: absolute; pointer-events: auto; border:1px solid var(--br); background: rgba(15,23,42,.6); color: #fff; border-radius: 12px; overflow: hidden; backdrop-filter: blur(8px); }
.home-widget-header { padding: 8px 10px; font-size: 12px; font-weight: 700; cursor: grab; background: rgba(59,130,246,.35); user-select:none; }
.home-widget-body { padding: 10px; font-size: 18px; }

</style>
</head>
<body>
Expand Down Expand Up @@ -270,6 +275,9 @@ <h3 style="margin-top:25px;">Bob</h3>
</div>
</div>

<h3 style="margin-top:25px;">Advanced</h3>
<a href="settings.html?tab=preference" class="legal-link">🧩 Widget Settings</a>

<h3 style="margin-top:25px;">Legal</h3>
<a href="privacypolicy.html" class="legal-link">🔒 Privacy Policy</a>
<a href="tos.html" class="legal-link">📜 Terms of Service</a>
Expand Down Expand Up @@ -301,6 +309,7 @@ <h2>Account</h2>
</div>

<!-- HOME PAGE CONTENT -->
<div id="widgetBoard" aria-label="Homepage widget board"></div>
<div class="home-inner">
<br><br>
<div class="clock-pill" id="clock">--:--</div>
Expand Down Expand Up @@ -336,6 +345,8 @@ <h2>Account</h2>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/wide.js"></script>
<script src="/assets/js/home.js"></script>
<script src="/assets/js/widgets.js"></script>
<script src="/assets/js/platform-v3.js"></script>
<script src="/assets/js/cookie.js"></script>
<script src="/assets/js/cursor.js"></script>

Expand Down
40 changes: 40 additions & 0 deletions settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,44 @@
</div>
</div>


<div class="st-card">
<div class="st-card-title">Widgets</div>
<form id="widgetForm" style="display:grid;gap:8px;">
<select id="widgetType" class="st-input">
<option value="weather">Weather widget</option>
<option value="time">Time widget</option>
<option value="note">Note widget</option>
</select>
<input id="widgetTitle" class="st-input" placeholder="Widget title" />
<div style="display:flex;gap:8px;">
<input id="widgetWidth" class="st-input" type="number" min="140" value="220" placeholder="Width" />
<input id="widgetHeight" class="st-input" type="number" min="80" value="130" placeholder="Height" />
</div>
<input id="widgetText" class="st-input" placeholder="Note text (for note widget)" />
<div style="display:flex;gap:8px;">
<select id="widgetUnit" class="st-input"><option value="C">Celsius</option><option value="F">Fahrenheit</option></select>
<input id="widgetTimezone" class="st-input" placeholder="Timezone (optional, e.g. America/New_York)" />
</div>
<input id="widgetLocale" class="st-input" placeholder="Locale (optional, e.g. en-US)" />
<select id="widgetShape" class="st-input"><option value="rounded">Rounded</option><option value="pill">Pill</option><option value="square">Square</option></select>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<label class="st-row-sub">Background <input id="widgetBgColor" type="color" value="#0f172a"/></label>
<label class="st-row-sub">Header <input id="widgetHeaderColor" type="color" value="#3b82f6"/></label>
<label class="st-row-sub">Text <input id="widgetTextColor" type="color" value="#ffffff"/></label>
<label class="st-row-sub">Border <input id="widgetBorderColor" type="color" value="#94a3b8"/></label>
</div>
<div style="display:flex;gap:8px;">
<input id="widgetRadius" class="st-input" type="number" min="0" max="48" value="12" placeholder="Corner radius" />
<input id="widgetFontSize" class="st-input" type="number" min="10" max="48" value="18" placeholder="Font size" />
<input id="widgetOpacity" class="st-input" type="number" min="0.2" max="1" step="0.05" value="0.85" placeholder="Opacity" />
</div>
<button class="st-btn primary" type="submit">Add Widget</button>
</form>
<div id="widgetList" style="margin-top:12px;"></div>
<div class="st-row-sub" style="margin-top:10px;">Tip: drag widgets around on the homepage after adding them.</div>
</div>

<!-- About -->
<div class="st-card">
<div class="st-card-title">About 360</div>
Expand Down Expand Up @@ -731,6 +769,8 @@
<script src="/assets/js/wide.js"></script>
<script src="/assets/js/cookie.js"></script>
<script src="/assets/js/cursor.js"></script>
<script src="/assets/js/widgets.js"></script>
<script src="/assets/js/platform-v3.js"></script>

<script>
// ────────────────────────────────────────
Expand Down