notion / index.html
Harry00's picture
Add 1 files
dcbce5a verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notion Lite - Local Note Taking</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
dark: {
800: '#1e293b',
900: '#0f172a',
}
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-in': 'slideIn 0.2s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideIn: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
}
}
}
}
}
</script>
<style>
.prose {
max-width: 100%;
}
.prose :where(h1):not(:where([class~="not-prose"] *)) {
font-size: 2rem;
margin-top: 0;
margin-bottom: 1rem;
font-weight: 700;
line-height: 1.2;
}
.prose :where(h2):not(:where([class~="not-prose"] *)) {
font-size: 1.5rem;
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
line-height: 1.3;
}
.prose :where(h3):not(:where([class~="not-prose"] *)) {
font-size: 1.25rem;
margin-top: 1.25rem;
margin-bottom: 0.75rem;
font-weight: 600;
line-height: 1.4;
}
.prose :where(p):not(:where([class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1rem;
}
.prose :where(ul):not(:where([class~="not-prose"] *)) {
list-style-type: disc;
padding-left: 1.5rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.prose :where(ol):not(:where([class~="not-prose"] *)) {
list-style-type: decimal;
padding-left: 1.5rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.prose :where(blockquote):not(:where([class~="not-prose"] *)) {
border-left: 4px solid #e5e7eb;
padding-left: 1rem;
margin-left: 0;
margin-top: 1rem;
margin-bottom: 1rem;
color: #6b7280;
}
.prose :where(code):not(:where([class~="not-prose"] *)) {
background-color: rgba(55, 65, 81, 0.1);
border-radius: 0.25rem;
padding: 0.2rem 0.4rem;
font-family: monospace;
}
.prose :where(pre):not(:where([class~="not-prose"] *)) {
background-color: rgba(55, 65, 81, 0.1);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
margin-top: 1rem;
margin-bottom: 1rem;
}
.prose :where(a):not(:where([class~="not-prose"] *)) {
color: #3b82f6;
text-decoration: underline;
}
.dark .prose :where(code):not(:where([class~="not-prose"] *)) {
background-color: rgba(209, 213, 219, 0.1);
}
.dark .prose :where(pre):not(:where([class~="not-prose"] *)) {
background-color: rgba(209, 213, 219, 0.1);
}
.dark .prose :where(blockquote):not(:where([class~="not-prose"] *)) {
border-left-color: #374151;
color: #9ca3af;
}
.editor-content {
min-height: calc(100vh - 200px);
}
.sidebar {
transition: transform 0.2s ease-in-out;
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
transform: translateX(-100%);
z-index: 40;
}
.sidebar-open {
transform: translateX(0);
}
}
.table-wrapper {
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #e5e7eb;
padding: 0.5rem;
text-align: left;
}
.dark th, .dark td {
border-color: #374151;
}
.to-do-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.to-do-checkbox {
margin-top: 0.25rem;
flex-shrink: 0;
}
.to-do-content {
flex-grow: 1;
}
.to-do-checked {
opacity: 0.6;
text-decoration: line-through;
}
.image-upload-wrapper {
position: relative;
margin: 1rem 0;
}
.image-upload {
width: 100%;
max-width: 100%;
border-radius: 0.5rem;
display: block;
}
.image-upload-actions {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
gap: 0.5rem;
opacity: 0;
transition: opacity 0.2s;
}
.image-upload-wrapper:hover .image-upload-actions {
opacity: 1;
}
.tooltip {
position: relative;
}
.tooltip-text {
visibility: hidden;
width: 120px;
background-color: #1e293b;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.dark .tooltip-text {
background-color: #f8fafc;
color: #0f172a;
}
</style>
</head>
<body class="bg-gray-50 dark:bg-dark-900 text-gray-800 dark:text-gray-200 transition-colors duration-200">
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<div class="sidebar w-64 bg-white dark:bg-dark-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-md bg-primary-500 flex items-center justify-center text-white">
<i class="fas fa-book"></i>
</div>
<h1 class="font-bold text-lg">Notion Lite</h1>
</div>
<button id="sidebar-close" class="md:hidden p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto p-2">
<div class="mb-4">
<div class="flex items-center justify-between px-2 py-1">
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Workspace</span>
<button id="add-page" class="text-gray-500 dark:text-gray-400 hover:text-primary-500 dark:hover:text-primary-400">
<i class="fas fa-plus"></i>
</button>
</div>
<div id="page-list" class="mt-1 space-y-1">
<!-- Pages will be loaded here -->
</div>
</div>
<div class="mb-4">
<div class="flex items-center justify-between px-2 py-1">
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Quick Links</span>
</div>
<div class="mt-1 space-y-1">
<a href="#" class="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 text-sm">
<i class="fas fa-inbox text-gray-500 dark:text-gray-400"></i>
<span>Inbox</span>
</a>
<a href="#" class="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 text-sm">
<i class="fas fa-star text-gray-500 dark:text-gray-400"></i>
<span>Favorites</span>
</a>
<a href="#" class="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 text-sm">
<i class="fas fa-trash text-gray-500 dark:text-gray-400"></i>
<span>Trash</span>
</a>
</div>
</div>
</div>
<div class="p-3 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center text-primary-600 dark:text-primary-300">
<i class="fas fa-user"></i>
</div>
<div class="flex-1">
<div class="text-sm font-medium">Guest User</div>
<div class="text-xs text-gray-500 dark:text-gray-400">Free Plan</div>
</div>
<button id="theme-toggle" class="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block"></i>
</button>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Top Bar -->
<div class="bg-white dark:bg-dark-800 border-b border-gray-200 dark:border-gray-700 p-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<button id="sidebar-toggle" class="md:hidden p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-bars"></i>
</button>
<div id="breadcrumbs" class="flex items-center gap-1 text-sm">
<!-- Breadcrumbs will be loaded here -->
</div>
</div>
<div class="flex items-center gap-2">
<button id="search-button" class="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-search"></i>
</button>
<button id="share-button" class="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-share-alt"></i>
</button>
<button id="more-options" class="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-ellipsis-h"></i>
</button>
</div>
</div>
<!-- Editor Area -->
<div class="flex-1 overflow-auto p-4 md:p-6">
<div id="editor" class="max-w-4xl mx-auto prose dark:prose-invert">
<div id="editor-content" class="editor-content" contenteditable="true">
<!-- Editor content will be loaded here -->
</div>
</div>
</div>
<!-- Floating Action Buttons -->
<div class="fixed bottom-6 right-6 flex flex-col gap-3">
<div class="tooltip">
<button id="add-block" class="w-12 h-12 rounded-full bg-primary-500 text-white flex items-center justify-center shadow-lg hover:bg-primary-600 transition-colors">
<i class="fas fa-plus"></i>
</button>
<span class="tooltip-text">Add block</span>
</div>
<div class="tooltip">
<button id="save-button" class="w-12 h-12 rounded-full bg-green-500 text-white flex items-center justify-center shadow-lg hover:bg-green-600 transition-colors">
<i class="fas fa-save"></i>
</button>
<span class="tooltip-text">Save changes</span>
</div>
</div>
</div>
</div>
<!-- Add Block Menu -->
<div id="block-menu" class="hidden fixed z-50 bg-white dark:bg-dark-800 rounded-lg shadow-xl w-64 border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="p-2 border-b border-gray-200 dark:border-gray-700">
<div class="relative">
<input type="text" placeholder="Search blocks..." class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary-500">
<i class="fas fa-search absolute right-3 top-2.5 text-gray-400"></i>
</div>
</div>
<div class="overflow-y-auto max-h-96">
<div class="p-1">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400 px-2 py-1">Basic Blocks</div>
<button class="block-menu-item" data-type="text">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-align-left text-gray-500 dark:text-gray-400"></i>
</div>
<span>Text</span>
</button>
<button class="block-menu-item" data-type="heading1">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-heading text-gray-500 dark:text-gray-400"></i>
</div>
<span>Heading 1</span>
</button>
<button class="block-menu-item" data-type="heading2">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-heading text-gray-500 dark:text-gray-400" style="font-size: 0.8em;"></i>
</div>
<span>Heading 2</span>
</button>
<button class="block-menu-item" data-type="heading3">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-heading text-gray-500 dark:text-gray-400" style="font-size: 0.6em;"></i>
</div>
<span>Heading 3</span>
</button>
<button class="block-menu-item" data-type="bullet-list">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-list-ul text-gray-500 dark:text-gray-400"></i>
</div>
<span>Bulleted list</span>
</button>
<button class="block-menu-item" data-type="number-list">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-list-ol text-gray-500 dark:text-gray-400"></i>
</div>
<span>Numbered list</span>
</button>
<button class="block-menu-item" data-type="to-do">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-check-square text-gray-500 dark:text-gray-400"></i>
</div>
<span>To-do list</span>
</button>
<button class="block-menu-item" data-type="quote">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-quote-right text-gray-500 dark:text-gray-400"></i>
</div>
<span>Quote</span>
</button>
<button class="block-menu-item" data-type="divider">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-minus text-gray-500 dark:text-gray-400"></i>
</div>
<span>Divider</span>
</button>
</div>
<div class="p-1">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400 px-2 py-1">Media</div>
<button class="block-menu-item" data-type="image">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-image text-gray-500 dark:text-gray-400"></i>
</div>
<span>Image</span>
</button>
<button class="block-menu-item" data-type="video">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-video text-gray-500 dark:text-gray-400"></i>
</div>
<span>Video</span>
</button>
<button class="block-menu-item" data-type="file">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-file text-gray-500 dark:text-gray-400"></i>
</div>
<span>File</span>
</button>
</div>
<div class="p-1">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400 px-2 py-1">Advanced</div>
<button class="block-menu-item" data-type="code">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-code text-gray-500 dark:text-gray-400"></i>
</div>
<span>Code</span>
</button>
<button class="block-menu-item" data-type="table">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-table text-gray-500 dark:text-gray-400"></i>
</div>
<span>Table</span>
</button>
<button class="block-menu-item" data-type="page">
<div class="w-6 h-6 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-file-alt text-gray-500 dark:text-gray-400"></i>
</div>
<span>Page</span>
</button>
</div>
</div>
</div>
<!-- Formatting Toolbar -->
<div id="formatting-toolbar" class="hidden fixed z-40 bg-white dark:bg-dark-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 p-1 flex items-center gap-1">
<button class="format-button" data-command="bold" title="Bold (Ctrl+B)">
<i class="fas fa-bold"></i>
</button>
<button class="format-button" data-command="italic" title="Italic (Ctrl+I)">
<i class="fas fa-italic"></i>
</button>
<button class="format-button" data-command="underline" title="Underline (Ctrl+U)">
<i class="fas fa-underline"></i>
</button>
<button class="format-button" data-command="strikeThrough" title="Strikethrough (Ctrl+Shift+X)">
<i class="fas fa-strikethrough"></i>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
<button class="format-button" data-command="justifyLeft" title="Align left">
<i class="fas fa-align-left"></i>
</button>
<button class="format-button" data-command="justifyCenter" title="Align center">
<i class="fas fa-align-center"></i>
</button>
<button class="format-button" data-command="justifyRight" title="Align right">
<i class="fas fa-align-right"></i>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
<button class="format-button" data-command="insertUnorderedList" title="Bullet list">
<i class="fas fa-list-ul"></i>
</button>
<button class="format-button" data-command="insertOrderedList" title="Numbered list">
<i class="fas fa-list-ol"></i>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
<button class="format-button" data-command="createLink" title="Create link">
<i class="fas fa-link"></i>
</button>
<button class="format-button" data-command="unlink" title="Remove link">
<i class="fas fa-unlink"></i>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
<button class="format-button" data-command="undo" title="Undo (Ctrl+Z)">
<i class="fas fa-undo"></i>
</button>
<button class="format-button" data-command="redo" title="Redo (Ctrl+Y)">
<i class="fas fa-redo"></i>
</button>
</div>
<!-- Image Upload Modal -->
<div id="image-upload-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium">Upload Image</h3>
<button id="close-image-modal" class="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mb-4">
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-4 text-center">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">Drag & drop your image here or click to browse</p>
<input type="file" id="image-input" accept="image/*" class="hidden">
<button id="browse-button" class="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600">
Browse Files
</button>
</div>
</div>
<div class="mb-4">
<label for="image-url" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Or enter image URL</label>
<input type="text" id="image-url" placeholder="https://example.com/image.jpg" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-700">
</div>
<div class="flex justify-end gap-2">
<button id="cancel-image-upload" class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md">
Cancel
</button>
<button id="confirm-image-upload" class="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600">
Insert Image
</button>
</div>
</div>
</div>
<!-- Add Page Modal -->
<div id="add-page-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium">Create New Page</h3>
<button id="close-page-modal" class="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mb-4">
<label for="page-title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Page Title</label>
<input type="text" id="page-title" placeholder="Untitled" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-700">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Parent Page</label>
<select id="parent-page" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-700">
<option value="">No parent (top level)</option>
<!-- Parent pages will be populated here -->
</select>
</div>
<div class="flex justify-end gap-2">
<button id="cancel-page-add" class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md">
Cancel
</button>
<button id="confirm-page-add" class="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600">
Create Page
</button>
</div>
</div>
</div>
<script>
// State management
const state = {
currentPageId: null,
pages: [],
showFormattingToolbar: false,
toolbarPosition: { x: 0, y: 0 },
blockMenuPosition: { x: 0, y: 0 },
lastSelection: null
};
// DOM Elements
const elements = {
editorContent: document.getElementById('editor-content'),
pageList: document.getElementById('page-list'),
breadcrumbs: document.getElementById('breadcrumbs'),
sidebar: document.querySelector('.sidebar'),
sidebarToggle: document.getElementById('sidebar-toggle'),
sidebarClose: document.getElementById('sidebar-close'),
themeToggle: document.getElementById('theme-toggle'),
addBlock: document.getElementById('add-block'),
blockMenu: document.getElementById('block-menu'),
formattingToolbar: document.getElementById('formatting-toolbar'),
formatButtons: document.querySelectorAll('.format-button'),
blockMenuItems: document.querySelectorAll('.block-menu-item'),
imageUploadModal: document.getElementById('image-upload-modal'),
imageInput: document.getElementById('image-input'),
browseButton: document.getElementById('browse-button'),
imageUrl: document.getElementById('image-url'),
confirmImageUpload: document.getElementById('confirm-image-upload'),
cancelImageUpload: document.getElementById('cancel-image-upload'),
closeImageModal: document.getElementById('close-image-modal'),
addPageButton: document.getElementById('add-page'),
addPageModal: document.getElementById('add-page-modal'),
pageTitle: document.getElementById('page-title'),
parentPage: document.getElementById('parent-page'),
confirmPageAdd: document.getElementById('confirm-page-add'),
cancelPageAdd: document.getElementById('cancel-page-add'),
closePageModal: document.getElementById('close-page-modal'),
saveButton: document.getElementById('save-button')
};
// Initialize the app
function init() {
loadPages();
setupEventListeners();
updateUI();
// Create a default page if none exists
if (state.pages.length === 0) {
createPage('Welcome to Notion Lite', '');
} else {
loadPage(state.pages[0].id);
}
}
// Load pages from local storage
function loadPages() {
const savedPages = localStorage.getItem('notion-lite-pages');
if (savedPages) {
state.pages = JSON.parse(savedPages);
renderPageList();
}
}
// Save pages to local storage
function savePages() {
localStorage.setItem('notion-lite-pages', JSON.stringify(state.pages));
}
// Create a new page
function createPage(title, parentId) {
const newPage = {
id: Date.now().toString(),
title: title,
parentId: parentId || null,
content: '<h1>' + title + '</h1><p>Start writing here...</p>',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
state.pages.push(newPage);
savePages();
renderPageList();
if (!parentId) {
loadPage(newPage.id);
}
return newPage;
}
// Load a page into the editor
function loadPage(pageId) {
const page = state.pages.find(p => p.id === pageId);
if (!page) return;
state.currentPageId = pageId;
elements.editorContent.innerHTML = page.content;
// Update breadcrumbs
updateBreadcrumbs(pageId);
// Update UI
updateUI();
}
// Save the current page
function saveCurrentPage() {
if (!state.currentPageId) return;
const pageIndex = state.pages.findIndex(p => p.id === state.currentPageId);
if (pageIndex === -1) return;
state.pages[pageIndex].content = elements.editorContent.innerHTML;
state.pages[pageIndex].updatedAt = new Date().toISOString();
savePages();
// Show save confirmation
const saveButton = elements.saveButton;
saveButton.innerHTML = '<i class="fas fa-check"></i>';
saveButton.classList.remove('bg-green-500', 'hover:bg-green-600');
saveButton.classList.add('bg-green-600');
setTimeout(() => {
saveButton.innerHTML = '<i class="fas fa-save"></i>';
saveButton.classList.remove('bg-green-600');
saveButton.classList.add('bg-green-500', 'hover:bg-green-600');
}, 2000);
}
// Update breadcrumbs
function updateBreadcrumbs(pageId) {
const page = state.pages.find(p => p.id === pageId);
if (!page) return;
let breadcrumbs = [];
let currentPage = page;
while (currentPage) {
breadcrumbs.unshift(currentPage);
currentPage = currentPage.parentId ? state.pages.find(p => p.id === currentPage.parentId) : null;
}
elements.breadcrumbs.innerHTML = '';
breadcrumbs.forEach((crumb, index) => {
const link = document.createElement('a');
link.href = '#';
link.className = 'hover:text-primary-500 dark:hover:text-primary-400';
link.textContent = crumb.title;
link.addEventListener('click', (e) => {
e.preventDefault();
loadPage(crumb.id);
});
elements.breadcrumbs.appendChild(link);
if (index < breadcrumbs.length - 1) {
const separator = document.createElement('span');
separator.className = 'text-gray-400';
separator.textContent = ' / ';
elements.breadcrumbs.appendChild(separator);
}
});
}
// Render the page list in the sidebar
function renderPageList() {
elements.pageList.innerHTML = '';
// Top-level pages
const topLevelPages = state.pages.filter(page => !page.parentId);
topLevelPages.forEach(page => {
renderPageListItem(page);
});
}
// Render a single page list item (recursive for children)
function renderPageListItem(page, depth = 0) {
const item = document.createElement('div');
item.className = `group flex items-center justify-between rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 ${state.currentPageId === page.id ? 'bg-gray-100 dark:bg-gray-700' : ''}`;
const leftSide = document.createElement('div');
leftSide.className = 'flex items-center flex-1 min-w-0';
leftSide.style.paddingLeft = `${depth * 12}px`;
const link = document.createElement('a');
link.href = '#';
link.className = 'flex items-center gap-2 py-1.5 px-2 flex-1 min-w-0';
link.innerHTML = `
<i class="fas fa-file-alt text-gray-500 dark:text-gray-400"></i>
<span class="truncate">${page.title}</span>
`;
link.addEventListener('click', (e) => {
e.preventDefault();
loadPage(page.id);
});
const rightSide = document.createElement('div');
rightSide.className = 'opacity-0 group-hover:opacity-100 pr-2';
const addButton = document.createElement('button');
addButton.className = 'p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400';
addButton.innerHTML = '<i class="fas fa-plus text-xs"></i>';
addButton.addEventListener('click', (e) => {
e.stopPropagation();
showAddPageModal(page.id);
});
rightSide.appendChild(addButton);
leftSide.appendChild(link);
item.appendChild(leftSide);
item.appendChild(rightSide);
elements.pageList.appendChild(item);
// Child pages
const childPages = state.pages.filter(p => p.parentId === page.id);
childPages.forEach(child => {
renderPageListItem(child, depth + 1);
});
}
// Update UI based on current state
function updateUI() {
// Highlight current page in sidebar
document.querySelectorAll('#page-list a').forEach(link => {
link.parentElement.parentElement.classList.remove('bg-gray-100', 'dark:bg-gray-700');
});
const currentLink = document.querySelector(`#page-list a[href="#"]`);
if (currentLink) {
currentLink.parentElement.parentElement.classList.add('bg-gray-100', 'dark:bg-gray-700');
}
}
// Show the add block menu
function showBlockMenu(x, y) {
state.blockMenuPosition = { x, y };
elements.blockMenu.style.left = `${x}px`;
elements.blockMenu.style.top = `${y}px`;
elements.blockMenu.classList.remove('hidden');
// Focus search input if available
const searchInput = elements.blockMenu.querySelector('input');
if (searchInput) {
searchInput.focus();
}
}
// Hide the add block menu
function hideBlockMenu() {
elements.blockMenu.classList.add('hidden');
}
// Show the formatting toolbar
function showFormattingToolbar(x, y) {
state.showFormattingToolbar = true;
state.toolbarPosition = { x, y };
elements.formattingToolbar.style.left = `${x}px`;
elements.formattingToolbar.style.top = `${y}px`;
elements.formattingToolbar.classList.remove('hidden');
}
// Hide the formatting toolbar
function hideFormattingToolbar() {
state.showFormattingToolbar = false;
elements.formattingToolbar.classList.add('hidden');
}
// Show image upload modal
function showImageUploadModal() {
elements.imageUploadModal.classList.remove('hidden');
}
// Hide image upload modal
function hideImageUploadModal() {
elements.imageUploadModal.classList.add('hidden');
elements.imageInput.value = '';
elements.imageUrl.value = '';
}
// Show add page modal
function showAddPageModal(parentId = null) {
elements.addPageModal.classList.remove('hidden');
elements.parentPage.value = parentId || '';
}
// Hide add page modal
function hideAddPageModal() {
elements.addPageModal.classList.add('hidden');
elements.pageTitle.value = '';
elements.parentPage.value = '';
}
// Insert a block at the current selection
function insertBlock(type) {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const block = document.createElement('div');
block.className = 'mb-4';
switch (type) {
case 'heading1':
block.innerHTML = '<h1>Heading 1</h1>';
break;
case 'heading2':
block.innerHTML = '<h2>Heading 2</h2>';
break;
case 'heading3':
block.innerHTML = '<h3>Heading 3</h3>';
break;
case 'bullet-list':
block.innerHTML = '<ul><li>List item</li></ul>';
break;
case 'number-list':
block.innerHTML = '<ol><li>List item</li></ol>';
break;
case 'to-do':
block.innerHTML = `
<div class="to-do-item">
<input type="checkbox" class="to-do-checkbox">
<div class="to-do-content" contenteditable="true">Todo item</div>
</div>
`;
break;
case 'quote':
block.innerHTML = '<blockquote>Quote</blockquote>';
break;
case 'divider':
block.innerHTML = '<hr class="border-t border-gray-300 dark:border-gray-600 my-4">';
break;
case 'image':
block.innerHTML = `
<div class="image-upload-wrapper">
<img src="https://via.placeholder.com/600x400?text=Click+to+upload+image" alt="Placeholder image" class="image-upload">
<div class="image-upload-actions">
<button class="p-1 bg-white dark:bg-gray-700 rounded-md shadow hover:bg-gray-100 dark:hover:bg-gray-600">
<i class="fas fa-edit text-gray-700 dark:text-gray-300"></i>
</button>
<button class="p-1 bg-white dark:bg-gray-700 rounded-md shadow hover:bg-gray-100 dark:hover:bg-gray-600">
<i class="fas fa-trash text-gray-700 dark:text-gray-300"></i>
</button>
</div>
</div>
`;
break;
case 'code':
block.innerHTML = '<pre><code>// Your code here</code></pre>';
break;
case 'table':
block.innerHTML = `
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
<th>Header 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
<td>Cell 3</td>
</tr>
<tr>
<td>Cell 4</td>
<td>Cell 5</td>
<td>Cell 6</td>
</tr>
</tbody>
</table>
</div>
`;
break;
default:
block.innerHTML = '<p>Text block</p>';
}
range.insertNode(block);
// Move cursor inside the new block
const editableElement = block.querySelector('[contenteditable="true"]') || block;
const newRange = document.createRange();
newRange.selectNodeContents(editableElement);
newRange.collapse(false);
selection.removeAllRanges();
selection.addRange(newRange);
editableElement.focus();
hideBlockMenu();
}
// Execute a formatting command
function executeFormatCommand(command, value = null) {
document.execCommand(command, false, value);
elements.editorContent.focus();
}
// Setup event listeners
function setupEventListeners() {
// Editor content events
elements.editorContent.addEventListener('input', () => {
// Auto-save after a delay
clearTimeout(window.saveTimeout);
window.saveTimeout = setTimeout(saveCurrentPage, 1000);
});
elements.editorContent.addEventListener('click', () => {
elements.editorContent.focus();
});
elements.editorContent.addEventListener('keydown', (e) => {
// Save on Ctrl+S
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveCurrentPage();
}
// Show block menu on / command
if (e.key === '/' && elements.editorContent === document.activeElement) {
e.preventDefault();
const range = window.getSelection().getRangeAt(0);
const rect = range.getBoundingClientRect();
showBlockMenu(rect.left, rect.bottom);
}
});
elements.editorContent.addEventListener('mouseup', (e) => {
const selection = window.getSelection();
if (selection.toString().length > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
showFormattingToolbar(rect.left, rect.top - 40);
state.lastSelection = selection;
} else {
hideFormattingToolbar();
}
});
// Sidebar events
elements.sidebarToggle.addEventListener('click', () => {
elements.sidebar.classList.add('sidebar-open');
});
elements.sidebarClose.addEventListener('click', () => {
elements.sidebar.classList.remove('sidebar-open');
});
// Theme toggle
elements.themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
});
// Add block button
elements.addBlock.addEventListener('click', () => {
const rect = elements.addBlock.getBoundingClientRect();
showBlockMenu(rect.left - 200, rect.top - 10);
});
// Block menu items
elements.blockMenuItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const type = item.dataset.type;
insertBlock(type);
});
});
// Format buttons
elements.formatButtons.forEach(button => {
button.addEventListener('click', () => {
executeFormatCommand(button.dataset.command);
});
});
// Click outside to hide menus
document.addEventListener('click', (e) => {
if (!elements.blockMenu.contains(e.target) && e.target !== elements.addBlock) {
hideBlockMenu();
}
if (!elements.formattingToolbar.contains(e.target) &&
!(state.lastSelection && state.lastSelection.toString().length > 0)) {
hideFormattingToolbar();
}
});
// Image upload modal events
elements.browseButton.addEventListener('click', () => {
elements.imageInput.click();
});
elements.imageInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
elements.imageUrl.value = event.target.result;
};
reader.readAsDataURL(file);
}
});
elements.confirmImageUpload.addEventListener('click', () => {
const imageUrl = elements.imageUrl.value.trim();
if (imageUrl) {
const selection = window.getSelection();
if (selection.rangeCount) {
const range = selection.getRangeAt(0);
const img = document.createElement('img');
img.src = imageUrl;
img.className = 'rounded-md max-w-full h-auto';
range.insertNode(img);
hideImageUploadModal();
}
}
});
elements.cancelImageUpload.addEventListener('click', hideImageUploadModal);
elements.closeImageModal.addEventListener('click', hideImageUploadModal);
// Add page modal events
elements.addPageButton.addEventListener('click', showAddPageModal);
elements.confirmPageAdd.addEventListener('click', () => {
const title = elements.pageTitle.value.trim() || 'Untitled';
const parentId = elements.parentPage.value || null;
createPage(title, parentId);
hideAddPageModal();
});
elements.cancelPageAdd.addEventListener('click', hideAddPageModal);
elements.closePageModal.addEventListener('click', hideAddPageModal);
// Save button
elements.saveButton.addEventListener('click', saveCurrentPage);
// Check for saved dark mode preference
if (localStorage.getItem('darkMode') === 'true') {
document.documentElement.classList.add('dark');
} else if (localStorage.getItem('darkMode') === 'false') {
document.documentElement.classList.remove('dark');
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
}
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=Harry00/notion" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>