mirror of
https://gitlab.com/octospacc/WhichNot.git
synced 2025-06-27 09:02:56 +02:00
First prototype
This commit is contained in:
361
index.html
Normal file
361
index.html
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WhatsApp-style Notes</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--whatsapp-green: #00a884;
|
||||||
|
--message-green: #d9fdd3;
|
||||||
|
--header-bg: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList {
|
||||||
|
width: 30%;
|
||||||
|
background: white;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatScreen {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #efeae2 /*url('https://static.whatsapp.net/rsrc.php/v4/y_/r/TPtFe9SpDSX.png')*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatHeader {
|
||||||
|
background: var(--header-bg);
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Message {
|
||||||
|
background: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
max-width: 70%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Message.sent {
|
||||||
|
background: var(--message-green);
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timestamp {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SendBar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EditArea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotebookEmoji {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SettingsModal {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0 1rem rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ChatList {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatScreen {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-chat .ChatScreen {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-chat .ChatList {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module">
|
||||||
|
import { Component, h, render, createContext } from 'https://esm.sh/preact';
|
||||||
|
import { createRef } from 'https://esm.sh/preact/compat';
|
||||||
|
import { useContext, useState, useEffect, useCallback, useRef } from 'https://esm.sh/preact/hooks';
|
||||||
|
import htm from 'https://esm.sh/htm';
|
||||||
|
const html = htm.bind(h);
|
||||||
|
window.Component = Component;
|
||||||
|
window.createRef = createRef;
|
||||||
|
window.createContext = createContext;
|
||||||
|
window.useContext = useContext;
|
||||||
|
window.useState = useState;
|
||||||
|
window.useEffect = useEffect;
|
||||||
|
window.useCallback = useCallback;
|
||||||
|
window.useRef = useRef;
|
||||||
|
window.render = render;
|
||||||
|
window.html = html;
|
||||||
|
// for (const script of ["Launcher", "Settings", "SystemUI", "index3"]) {
|
||||||
|
// document.body.appendChild(Object.assign(document.createElement('link'), { rel: "stylesheet", href: `${script}.css` }));
|
||||||
|
// document.body.appendChild(Object.assign(document.createElement('script'), { src: `${script}.js` }));
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
import { h, render, createContext } from 'https://esm.sh/preact';
|
||||||
|
import { useContext, useState, useEffect, useCallback } from 'https://esm.sh/preact/hooks';
|
||||||
|
import htm from 'https://esm.sh/htm';
|
||||||
|
|
||||||
|
const AppContext = createContext();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
notebooks: [],
|
||||||
|
messages: {},
|
||||||
|
selectedNotebook: null,
|
||||||
|
editingMessage: null,
|
||||||
|
showSettings: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadFromStorage = useCallback(() => {
|
||||||
|
const notebooks = JSON.parse(localStorage.getItem('notebooks') || '[]');
|
||||||
|
const messages = JSON.parse(localStorage.getItem('messages') || '{}');
|
||||||
|
setState(prev => ({ ...prev, notebooks, messages }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { loadFromStorage(); }, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('notebooks', JSON.stringify(state.notebooks));
|
||||||
|
localStorage.setItem('messages', JSON.stringify(state.messages));
|
||||||
|
}, [state.notebooks, state.messages]);
|
||||||
|
|
||||||
|
const createNotebook = useCallback(() => {
|
||||||
|
const newNotebook = {
|
||||||
|
id: Date.now(),
|
||||||
|
name: 'New Notebook',
|
||||||
|
emoji: '📒',
|
||||||
|
color: '#ffffff',
|
||||||
|
lastModified: Date.now(),
|
||||||
|
lastMessage: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
notebooks: [...prev.notebooks, newNotebook],
|
||||||
|
messages: { ...prev.messages, [newNotebook.id]: [] }
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = { state, setState, createNotebook };
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<${AppContext.Provider} value=${contextValue}>
|
||||||
|
<div class="App ${state.selectedNotebook ? 'show-chat' : ''}">
|
||||||
|
<${ChatList} />
|
||||||
|
<${ChatScreen} />
|
||||||
|
${state.showSettings && html`<${SettingsModal} />`}
|
||||||
|
</div>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatList() {
|
||||||
|
const { state, setState, createNotebook } = useContext(AppContext);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="ChatList">
|
||||||
|
<button onClick=${createNotebook}>+ New Notebook</button>
|
||||||
|
${state.notebooks.sort((a, b) => b.lastModified - a.lastModified).map(notebook => html`
|
||||||
|
<div class="ChatSlip" onClick=${() => setState(prev => ({ ...prev, selectedNotebook: notebook.id }))}>
|
||||||
|
<div class="NotebookEmoji" style=${{ background: notebook.color }}>
|
||||||
|
${notebook.emoji}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>${notebook.name}</h3>
|
||||||
|
<p>${notebook.lastMessage || 'No messages'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatScreen() {
|
||||||
|
const { state, setState } = useContext(AppContext);
|
||||||
|
const inputRef = useRef();
|
||||||
|
const notebook = state.notebooks.find(n => n.id === state.selectedNotebook);
|
||||||
|
const messages = state.messages[state.selectedNotebook] || [];
|
||||||
|
|
||||||
|
const sendMessage = useCallback(() => {
|
||||||
|
const content = inputRef.current.value.trim();
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
setState(prev => {
|
||||||
|
const newMessages = state.editingMessage !== null
|
||||||
|
? prev.messages[prev.selectedNotebook].map((msg, idx) =>
|
||||||
|
idx === state.editingMessage ? { ...msg, content, edited: true } : msg
|
||||||
|
)
|
||||||
|
: [...(prev.messages[prev.selectedNotebook] || []), {
|
||||||
|
content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
edited: false
|
||||||
|
}];
|
||||||
|
|
||||||
|
const updatedNotebooks = prev.notebooks.map(n =>
|
||||||
|
n.id === prev.selectedNotebook ? {
|
||||||
|
...n,
|
||||||
|
lastModified: Date.now(),
|
||||||
|
lastMessage: content
|
||||||
|
} : n
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
messages: { ...prev.messages, [prev.selectedNotebook]: newMessages },
|
||||||
|
notebooks: updatedNotebooks,
|
||||||
|
editingMessage: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
inputRef.current.value = '';
|
||||||
|
}, [state.selectedNotebook, state.editingMessage]);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="ChatScreen">
|
||||||
|
<div class="ChatHeader">
|
||||||
|
<button onClick=${() => setState(prev => ({ ...prev, selectedNotebook: null }))}>←</button>
|
||||||
|
<div class="NotebookEmoji" style=${{ background: notebook?.color }}>${notebook?.emoji}</div>
|
||||||
|
<h3>${notebook?.name}</h3>
|
||||||
|
<button onClick=${() => setState(prev => ({ ...prev, showSettings: true }))}>⚙️</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="Messages">
|
||||||
|
${messages.map((msg, index) => html`
|
||||||
|
<div class="Message" onClick=${() => setState(prev => ({ ...prev, editingMessage: index }))}>
|
||||||
|
${msg.content}
|
||||||
|
<div class="Timestamp">
|
||||||
|
${new Date(msg.timestamp).toLocaleString()}
|
||||||
|
${msg.edited ? ' (edited)' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="SendBar">
|
||||||
|
<textarea
|
||||||
|
ref=${inputRef}
|
||||||
|
class="EditArea"
|
||||||
|
value=${state.editingMessage !== null ? messages[state.editingMessage]?.content : ''}
|
||||||
|
onKeyPress=${e => e.key === 'Enter' && !e.shiftKey && sendMessage()}
|
||||||
|
/>
|
||||||
|
<button onClick=${sendMessage}>
|
||||||
|
${state.editingMessage !== null ? 'Save' : 'Send'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsModal() {
|
||||||
|
const { state, setState } = useContext(AppContext);
|
||||||
|
const notebook = state.notebooks.find(n => n.id === state.selectedNotebook);
|
||||||
|
const [form, setForm] = useState({ ...notebook });
|
||||||
|
|
||||||
|
const saveSettings = useCallback(() => {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
notebooks: prev.notebooks.map(n => n.id === notebook.id ? form : n),
|
||||||
|
showSettings: false
|
||||||
|
}));
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
const deleteNotebook = useCallback(() => {
|
||||||
|
if (confirm('Delete this notebook?')) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
notebooks: prev.notebooks.filter(n => n.id !== notebook.id),
|
||||||
|
messages: { ...prev.messages, [notebook.id]: undefined },
|
||||||
|
selectedNotebook: null,
|
||||||
|
showSettings: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [notebook]);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="SettingsModal">
|
||||||
|
<h3>Settings</h3>
|
||||||
|
<label>
|
||||||
|
Name:
|
||||||
|
<input value=${form.name} onChange=${e => setForm(prev => ({ ...prev, name: e.target.value }))} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Emoji:
|
||||||
|
<input value=${form.emoji} maxLength="2"
|
||||||
|
onChange=${e => setForm(prev => ({ ...prev, emoji: e.target.value }))} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Color:
|
||||||
|
<input type="color" value=${form.color}
|
||||||
|
onChange=${e => setForm(prev => ({ ...prev, color: e.target.value }))} />
|
||||||
|
</label>
|
||||||
|
<button onClick=${saveSettings}>Save</button>
|
||||||
|
<button onClick=${deleteNotebook} style=${{ color: 'red' }}>Delete</button>
|
||||||
|
<button onClick=${() => setState(prev => ({ ...prev, showSettings: false }))}>Close</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(html`<${App} />`, document.body);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user