change style of button
This commit is contained in:
@@ -1,483 +1,472 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Notes')
|
||||
@section('page-title', '📝 Notes')
|
||||
@section('page-title', 'Notes Management 📝')
|
||||
@section('page-subtitle', 'Quick notes, checklists & reminders')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div x-data="notesApp({{ json_encode($notes) }})" x-cloak>
|
||||
<div x-data="notesApp({{ json_encode($notes) }})" x-cloak>
|
||||
|
||||
{{-- ── Toolbar ──────────────────────────────────────────────── --}}
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<button @click="openNew()"
|
||||
class="btn-sakura text-sm px-4 py-2 flex items-center gap-1.5 shrink-0">
|
||||
+ New Note
|
||||
</button>
|
||||
{{-- ── Toolbar ──────────────────────────────────────────────── --}}
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<button @click="openNew()" class="btn-sakura text-sm px-4 py-2 flex items-center gap-1.5 shrink-0">
|
||||
+ New Note
|
||||
</button>
|
||||
|
||||
{{-- Search --}}
|
||||
<div class="relative flex-1 max-w-xs">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></span>
|
||||
<input type="text" x-model="searchQuery" placeholder="🔍Search notes…"
|
||||
class="input-soft pl-9 text-sm py-2 w-full">
|
||||
{{-- Search --}}
|
||||
<div class="relative flex-1 max-w-xs">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></span>
|
||||
<input type="text" x-model="searchQuery" placeholder="🔍Search notes…"
|
||||
class="input-soft pl-9 text-sm py-2 w-full">
|
||||
</div>
|
||||
|
||||
{{-- Count --}}
|
||||
<span class="ml-auto text-xs text-gray-400 shrink-0"
|
||||
x-text="notes.length + ' note' + (notes.length !== 1 ? 's' : '')"></span>
|
||||
</div>
|
||||
|
||||
{{-- Count --}}
|
||||
<span class="ml-auto text-xs text-gray-400 shrink-0"
|
||||
x-text="notes.length + ' note' + (notes.length !== 1 ? 's' : '')"></span>
|
||||
</div>
|
||||
{{-- ── Pinned section ───────────────────────────────────────── --}}
|
||||
<template x-if="filteredPinned.length > 0">
|
||||
<div class="mb-5">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
|
||||
📌 Pinned
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<template x-for="note in filteredPinned" :key="note.id">
|
||||
<div class="rounded-3xl p-4 flex flex-col gap-2 shadow-sm border border-black/5 cursor-pointer
|
||||
hover:shadow-md transition-shadow duration-200 group relative"
|
||||
:style="`background:${note.color || '#FFFFFF'}`" @click="openEdit(note)">
|
||||
|
||||
{{-- ── Pinned section ───────────────────────────────────────── --}}
|
||||
<template x-if="filteredPinned.length > 0">
|
||||
<div class="mb-5">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
|
||||
📌 Pinned
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<template x-for="note in filteredPinned" :key="note.id">
|
||||
<div class="rounded-3xl p-4 flex flex-col gap-2 shadow-sm border border-black/5 cursor-pointer
|
||||
hover:shadow-md transition-shadow duration-200 group relative"
|
||||
:style="`background:${note.color || '#FFFFFF'}`"
|
||||
@click="openEdit(note)">
|
||||
{{-- Pin badge --}}
|
||||
<div
|
||||
class="absolute top-2.5 right-2.5 text-xs text-amber-400 opacity-60 group-hover:opacity-100">
|
||||
📌</div>
|
||||
|
||||
{{-- Pin badge --}}
|
||||
<div class="absolute top-2.5 right-2.5 text-xs text-amber-400 opacity-60 group-hover:opacity-100">📌</div>
|
||||
{{-- Title --}}
|
||||
<p class="font-bold text-gray-800 text-sm leading-snug pr-5 line-clamp-2"
|
||||
x-text="note.title || 'Untitled'"></p>
|
||||
|
||||
{{-- Title --}}
|
||||
<p class="font-bold text-gray-800 text-sm leading-snug pr-5 line-clamp-2"
|
||||
x-text="note.title || 'Untitled'"></p>
|
||||
{{-- Content preview --}}
|
||||
<div class="flex-1 text-xs text-gray-500 space-y-0.5 line-clamp-4">
|
||||
<template x-for="(block, i) in (note.content || []).slice(0, 4)" :key="i">
|
||||
<div class="flex items-start gap-1.5 min-w-0">
|
||||
<template x-if="block.type === 'bullet'">
|
||||
<span class="shrink-0 text-gray-400 mt-px">•</span>
|
||||
</template>
|
||||
<template x-if="block.type === 'checkbox'">
|
||||
<span class="shrink-0 mt-px" x-text="block.checked ? '☑' : '☐'"></span>
|
||||
</template>
|
||||
<span class="truncate"
|
||||
:class="block.type === 'checkbox' && block.checked ? 'line-through text-gray-400' : ''"
|
||||
x-text="block.text || '…'"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Content preview --}}
|
||||
<div class="flex-1 text-xs text-gray-500 space-y-0.5 line-clamp-4">
|
||||
<template x-for="(block, i) in (note.content || []).slice(0, 4)" :key="i">
|
||||
<div class="flex items-start gap-1.5 min-w-0">
|
||||
<template x-if="block.type === 'bullet'">
|
||||
<span class="shrink-0 text-gray-400 mt-px">•</span>
|
||||
</template>
|
||||
<template x-if="block.type === 'checkbox'">
|
||||
<span class="shrink-0 mt-px" x-text="block.checked ? '☑' : '☐'"></span>
|
||||
</template>
|
||||
<span class="truncate"
|
||||
:class="block.type === 'checkbox' && block.checked ? 'line-through text-gray-400' : ''"
|
||||
x-text="block.text || '…'"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<span class="text-xs text-gray-400" x-text="formatDate(note.updated_at)"></span>
|
||||
<button @click.stop="deleteNote(note)"
|
||||
{{-- Footer --}}
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<span class="text-xs text-gray-400" x-text="formatDate(note.updated_at)"></span>
|
||||
<button @click.stop="deleteNote(note)"
|
||||
class="opacity-0 group-hover:opacity-100 text-xs text-red-400 hover:text-red-600 transition-opacity leading-none px-1">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
{{-- ── All / Other notes ────────────────────────────────────── --}}
|
||||
<template x-if="filteredUnpinned.length > 0 || (filteredPinned.length > 0)">
|
||||
<div>
|
||||
<template x-if="filteredPinned.length > 0 && filteredUnpinned.length > 0">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Others</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<template x-for="note in filteredUnpinned" :key="note.id">
|
||||
<div class="rounded-3xl p-4 flex flex-col gap-2 shadow-sm border border-black/5 cursor-pointer
|
||||
hover:shadow-md transition-shadow duration-200 group"
|
||||
:style="`background:${note.color || '#FFFFFF'}`"
|
||||
@click="openEdit(note)">
|
||||
{{-- ── All / Other notes ────────────────────────────────────── --}}
|
||||
<template x-if="filteredUnpinned.length > 0 || (filteredPinned.length > 0)">
|
||||
<div>
|
||||
<template x-if="filteredPinned.length > 0 && filteredUnpinned.length > 0">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Others</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<template x-for="note in filteredUnpinned" :key="note.id">
|
||||
<div class="rounded-3xl p-4 flex flex-col gap-2 shadow-sm border border-black/5 cursor-pointer
|
||||
hover:shadow-md transition-shadow duration-200 group"
|
||||
:style="`background:${note.color || '#FFFFFF'}`" @click="openEdit(note)">
|
||||
|
||||
{{-- Title --}}
|
||||
<p class="font-bold text-gray-800 text-sm leading-snug line-clamp-2"
|
||||
x-text="note.title || 'Untitled'"></p>
|
||||
{{-- Title --}}
|
||||
<p class="font-bold text-gray-800 text-sm leading-snug line-clamp-2"
|
||||
x-text="note.title || 'Untitled'"></p>
|
||||
|
||||
{{-- Content preview --}}
|
||||
<div class="flex-1 text-xs text-gray-500 space-y-0.5">
|
||||
<template x-for="(block, i) in (note.content || []).slice(0, 4)" :key="i">
|
||||
<div class="flex items-start gap-1.5 min-w-0">
|
||||
<template x-if="block.type === 'bullet'">
|
||||
<span class="shrink-0 text-gray-400 mt-px">•</span>
|
||||
</template>
|
||||
<template x-if="block.type === 'checkbox'">
|
||||
<span class="shrink-0 mt-px" x-text="block.checked ? '☑' : '☐'"></span>
|
||||
</template>
|
||||
<span class="truncate"
|
||||
:class="block.type === 'checkbox' && block.checked ? 'line-through text-gray-400' : ''"
|
||||
x-text="block.text || '…'"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{{-- Content preview --}}
|
||||
<div class="flex-1 text-xs text-gray-500 space-y-0.5">
|
||||
<template x-for="(block, i) in (note.content || []).slice(0, 4)" :key="i">
|
||||
<div class="flex items-start gap-1.5 min-w-0">
|
||||
<template x-if="block.type === 'bullet'">
|
||||
<span class="shrink-0 text-gray-400 mt-px">•</span>
|
||||
</template>
|
||||
<template x-if="block.type === 'checkbox'">
|
||||
<span class="shrink-0 mt-px" x-text="block.checked ? '☑' : '☐'"></span>
|
||||
</template>
|
||||
<span class="truncate"
|
||||
:class="block.type === 'checkbox' && block.checked ? 'line-through text-gray-400' : ''"
|
||||
x-text="block.text || '…'"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<span class="text-xs text-gray-400" x-text="formatDate(note.updated_at)"></span>
|
||||
<button @click.stop="deleteNote(note)"
|
||||
{{-- Footer --}}
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<span class="text-xs text-gray-400" x-text="formatDate(note.updated_at)"></span>
|
||||
<button @click.stop="deleteNote(note)"
|
||||
class="opacity-0 group-hover:opacity-100 text-xs text-red-400 hover:text-red-600 transition-opacity leading-none px-1">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
{{-- ── Empty state ──────────────────────────────────────────── --}}
|
||||
<template x-if="notes.length === 0">
|
||||
<div class="text-center py-20 text-gray-400">
|
||||
<span class="text-5xl block mb-3">📝</span>
|
||||
<p class="text-sm font-medium mb-4">No notes yet.</p>
|
||||
<button @click="openNew()" class="btn-sakura text-sm">Write your first note</button>
|
||||
</div>
|
||||
</template>
|
||||
{{-- ── Empty state ──────────────────────────────────────────── --}}
|
||||
<template x-if="notes.length === 0">
|
||||
<div class="text-center py-20 text-gray-400">
|
||||
<span class="text-5xl block mb-3">📝</span>
|
||||
<p class="text-sm font-medium mb-4">No notes yet.</p>
|
||||
<button @click="openNew()" class="btn-sakura text-sm">Write your first note</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="notes.length > 0 && filteredPinned.length === 0 && filteredUnpinned.length === 0">
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<span class="text-3xl block mb-2">🔍</span>
|
||||
<p class="text-sm">No notes match "<span x-text="searchQuery"></span>"</p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="notes.length > 0 && filteredPinned.length === 0 && filteredUnpinned.length === 0">
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<span class="text-3xl block mb-2">🔍</span>
|
||||
<p class="text-sm">No notes match "<span x-text="searchQuery"></span>"</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════
|
||||
Note Editor Modal
|
||||
════════════════════════════════════════════════════════════ --}}
|
||||
<div x-show="showModal"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/40 px-0 sm:px-4"
|
||||
style="display:none"
|
||||
@keydown.escape.window="showModal = false">
|
||||
{{-- ════════════════════════════════════════════════════════════
|
||||
Note Editor Modal
|
||||
════════════════════════════════════════════════════════════ --}}
|
||||
<div x-show="showModal" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/40 px-0 sm:px-4"
|
||||
style="display:none" @keydown.escape.window="showModal = false">
|
||||
|
||||
<div class="w-full sm:max-w-lg rounded-t-3xl sm:rounded-3xl shadow-2xl flex flex-col overflow-hidden"
|
||||
:style="`background:${editorColor || '#FFFFFF'}`"
|
||||
style="max-height:90vh"
|
||||
@click.stop>
|
||||
<div class="w-full sm:max-w-lg rounded-t-3xl sm:rounded-3xl shadow-2xl flex flex-col overflow-hidden"
|
||||
:style="`background:${editorColor || '#FFFFFF'}`" style="max-height:90vh" @click.stop>
|
||||
|
||||
{{-- ── Modal Header ─────────────────────────────── --}}
|
||||
<div class="flex items-center gap-2 px-4 pt-4 pb-3 border-b border-black/5 shrink-0">
|
||||
{{-- ── Modal Header ─────────────────────────────── --}}
|
||||
<div class="flex items-center gap-2 px-4 pt-4 pb-3 border-b border-black/5 shrink-0">
|
||||
|
||||
{{-- Color swatches --}}
|
||||
<div class="flex gap-1.5 flex-wrap">
|
||||
<template x-for="c in noteColors" :key="c">
|
||||
<button @click="editorColor = c" type="button"
|
||||
{{-- Color swatches --}}
|
||||
<div class="flex gap-1.5 flex-wrap">
|
||||
<template x-for="c in noteColors" :key="c">
|
||||
<button @click="editorColor = c" type="button"
|
||||
class="w-5 h-5 rounded-full border-2 transition-transform hover:scale-110 shrink-0"
|
||||
:style="`background:${c}`"
|
||||
:class="editorColor === c ? 'border-gray-500 scale-110' : 'border-gray-200'">
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Pin toggle --}}
|
||||
<button @click="editorPinned = !editorPinned" type="button"
|
||||
{{-- Pin toggle --}}
|
||||
<button @click="editorPinned = !editorPinned" type="button"
|
||||
class="ml-auto text-sm px-2.5 py-1 rounded-full border transition-colors shrink-0"
|
||||
:class="editorPinned ? 'border-amber-300 bg-amber-50 text-amber-600' : 'border-gray-200 text-gray-400 hover:border-amber-200 hover:text-amber-400'">
|
||||
📌 <span x-text="editorPinned ? 'Pinned' : 'Pin'"></span>
|
||||
</button>
|
||||
📌 <span x-text="editorPinned ? 'Pinned' : 'Pin'"></span>
|
||||
</button>
|
||||
|
||||
{{-- Close --}}
|
||||
<button @click="showModal = false" type="button"
|
||||
{{-- Close --}}
|
||||
<button @click="showModal = false" type="button"
|
||||
class="text-gray-400 hover:text-gray-600 text-xl leading-none shrink-0 ml-1">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Scrollable Body ──────────────────────────── --}}
|
||||
<div class="flex-1 overflow-y-auto px-4 py-3 space-y-1">
|
||||
{{-- ── Scrollable Body ──────────────────────────── --}}
|
||||
<div class="flex-1 overflow-y-auto px-4 py-3 space-y-1">
|
||||
|
||||
{{-- Title --}}
|
||||
<input type="text" x-model="editorTitle" x-ref="titleInput"
|
||||
placeholder="Note title…"
|
||||
class="w-full bg-transparent border-none outline-none font-bold text-gray-800 text-lg placeholder-gray-300 mb-2"
|
||||
@keydown.enter.prevent="focusFirstBlock()">
|
||||
{{-- Title --}}
|
||||
<input type="text" x-model="editorTitle" x-ref="titleInput" placeholder="Note title…"
|
||||
class="w-full bg-transparent border-none outline-none font-bold text-gray-800 text-lg placeholder-gray-300 mb-2"
|
||||
@keydown.enter.prevent="focusFirstBlock()">
|
||||
|
||||
{{-- Blocks --}}
|
||||
<template x-for="(block, idx) in editorBlocks" :key="block.id">
|
||||
<div class="flex items-center gap-2 group/block py-0.5">
|
||||
{{-- Blocks --}}
|
||||
<template x-for="(block, idx) in editorBlocks" :key="block.id">
|
||||
<div class="flex items-center gap-2 group/block py-0.5">
|
||||
|
||||
{{-- Left side: bullet / checkbox / empty --}}
|
||||
<template x-if="block.type === 'bullet'">
|
||||
<span class="text-gray-400 text-sm shrink-0 select-none mt-px">•</span>
|
||||
</template>
|
||||
<template x-if="block.type === 'checkbox'">
|
||||
<input type="checkbox" x-model="block.checked"
|
||||
class="w-4 h-4 accent-pink-400 shrink-0 cursor-pointer mt-px"
|
||||
@click.stop>
|
||||
</template>
|
||||
<template x-if="block.type === 'paragraph'">
|
||||
<span class="w-4 shrink-0"></span>
|
||||
</template>
|
||||
{{-- Left side: bullet / checkbox / empty --}}
|
||||
<template x-if="block.type === 'bullet'">
|
||||
<span class="text-gray-400 text-sm shrink-0 select-none mt-px">•</span>
|
||||
</template>
|
||||
<template x-if="block.type === 'checkbox'">
|
||||
<input type="checkbox" x-model="block.checked"
|
||||
class="w-4 h-4 accent-pink-400 shrink-0 cursor-pointer mt-px" @click.stop>
|
||||
</template>
|
||||
<template x-if="block.type === 'paragraph'">
|
||||
<span class="w-4 shrink-0"></span>
|
||||
</template>
|
||||
|
||||
{{-- Text input --}}
|
||||
<input type="text"
|
||||
x-model="block.text"
|
||||
:placeholder="block.type === 'paragraph' ? 'Add text…' : block.type === 'bullet' ? 'List item…' : 'To-do item…'"
|
||||
:class="block.type === 'checkbox' && block.checked ? 'line-through text-gray-400' : 'text-gray-700'"
|
||||
class="flex-1 bg-transparent border-none outline-none text-sm placeholder-gray-300 py-0.5"
|
||||
:data-block-id="block.id"
|
||||
@keydown.enter.prevent="insertBlockAfter(idx)"
|
||||
@keydown.backspace="handleBackspace($event, block, idx)">
|
||||
{{-- Text input --}}
|
||||
<input type="text" x-model="block.text"
|
||||
:placeholder="block.type === 'paragraph' ? 'Add text…' : block.type === 'bullet' ? 'List item…' : 'To-do item…'"
|
||||
:class="block.type === 'checkbox' && block.checked ? 'line-through text-gray-400' : 'text-gray-700'"
|
||||
class="flex-1 bg-transparent border-none outline-none text-sm placeholder-gray-300 py-0.5"
|
||||
:data-block-id="block.id" @keydown.enter.prevent="insertBlockAfter(idx)"
|
||||
@keydown.backspace="handleBackspace($event, block, idx)">
|
||||
|
||||
{{-- Block type switcher (hover) --}}
|
||||
<div class="flex gap-0.5 opacity-0 group-hover/block:opacity-100 transition-opacity shrink-0">
|
||||
<button @click.stop="changeType(idx, 'paragraph')" type="button"
|
||||
{{-- Block type switcher (hover) --}}
|
||||
<div class="flex gap-0.5 opacity-0 group-hover/block:opacity-100 transition-opacity shrink-0">
|
||||
<button @click.stop="changeType(idx, 'paragraph')" type="button"
|
||||
:class="block.type === 'paragraph' ? 'text-pink-500' : 'text-gray-300 hover:text-gray-500'"
|
||||
class="text-xs px-1 leading-none" title="Text">T</button>
|
||||
<button @click.stop="changeType(idx, 'bullet')" type="button"
|
||||
<button @click.stop="changeType(idx, 'bullet')" type="button"
|
||||
:class="block.type === 'bullet' ? 'text-pink-500' : 'text-gray-300 hover:text-gray-500'"
|
||||
class="text-xs px-1 leading-none" title="Bullet">•</button>
|
||||
<button @click.stop="changeType(idx, 'checkbox')" type="button"
|
||||
<button @click.stop="changeType(idx, 'checkbox')" type="button"
|
||||
:class="block.type === 'checkbox' ? 'text-pink-500' : 'text-gray-300 hover:text-gray-500'"
|
||||
class="text-xs px-1 leading-none" title="Checkbox">☐</button>
|
||||
<button @click.stop="removeBlock(idx)" type="button"
|
||||
class="text-xs px-1 leading-none text-gray-300 hover:text-red-400 ml-1" title="Remove">✕</button>
|
||||
<button @click.stop="removeBlock(idx)" type="button"
|
||||
class="text-xs px-1 leading-none text-gray-300 hover:text-red-400 ml-1"
|
||||
title="Remove">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
{{-- Add block toolbar --}}
|
||||
<div class="flex gap-2 pt-3 border-t border-black/5 mt-2">
|
||||
<button @click="addBlock('paragraph')" type="button"
|
||||
{{-- Add block toolbar --}}
|
||||
<div class="flex gap-2 pt-3 border-t border-black/5 mt-2">
|
||||
<button @click="addBlock('paragraph')" type="button"
|
||||
class="flex items-center gap-1 text-xs px-3 py-1.5 rounded-full border border-gray-200 text-gray-500 hover:bg-white/60 hover:border-gray-300 transition-colors">
|
||||
<span class="font-semibold">T</span> Text
|
||||
<span class="font-semibold">T</span> Text
|
||||
</button>
|
||||
<button @click="addBlock('bullet')" type="button"
|
||||
class="flex items-center gap-1 text-xs px-3 py-1.5 rounded-full border border-gray-200 text-gray-500 hover:bg-white/60 hover:border-gray-300 transition-colors">
|
||||
• Bullet
|
||||
</button>
|
||||
<button @click="addBlock('checkbox')" type="button"
|
||||
class="flex items-center gap-1 text-xs px-3 py-1.5 rounded-full border border-gray-200 text-gray-500 hover:bg-white/60 hover:border-gray-300 transition-colors">
|
||||
☐ Checkbox
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Modal Footer ─────────────────────────────── --}}
|
||||
<div class="flex gap-2 px-4 pb-5 pt-3 border-t border-black/5 shrink-0">
|
||||
<button @click="saveNote()" type="button"
|
||||
class="btn-sakura flex-1 py-2.5 text-sm flex items-center justify-center gap-2" :disabled="saving">
|
||||
<span x-show="!saving">💾 Save Note</span>
|
||||
<span x-show="saving" class="flex items-center gap-1.5"><span
|
||||
class="animate-spin inline-block">🐾</span> Saving…</span>
|
||||
</button>
|
||||
<button @click="addBlock('bullet')" type="button"
|
||||
class="flex items-center gap-1 text-xs px-3 py-1.5 rounded-full border border-gray-200 text-gray-500 hover:bg-white/60 hover:border-gray-300 transition-colors">
|
||||
• Bullet
|
||||
</button>
|
||||
<button @click="addBlock('checkbox')" type="button"
|
||||
class="flex items-center gap-1 text-xs px-3 py-1.5 rounded-full border border-gray-200 text-gray-500 hover:bg-white/60 hover:border-gray-300 transition-colors">
|
||||
☐ Checkbox
|
||||
<button @click="showModal = false" type="button"
|
||||
class="flex-1 py-2.5 rounded-full border border-pink-200 text-gray-600 text-sm hover:bg-pink-50 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Modal Footer ─────────────────────────────── --}}
|
||||
<div class="flex gap-2 px-4 pb-5 pt-3 border-t border-black/5 shrink-0">
|
||||
<button @click="saveNote()" type="button"
|
||||
class="btn-sakura flex-1 py-2.5 text-sm flex items-center justify-center gap-2"
|
||||
:disabled="saving">
|
||||
<span x-show="!saving">💾 Save Note</span>
|
||||
<span x-show="saving" class="flex items-center gap-1.5"><span class="animate-spin inline-block">🐾</span> Saving…</span>
|
||||
</button>
|
||||
<button @click="showModal = false" type="button"
|
||||
class="flex-1 py-2.5 rounded-full border border-pink-200 text-gray-600 text-sm hover:bg-pink-50 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>{{-- end x-data --}}
|
||||
</div>{{-- end x-data --}}
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function notesApp(initialNotes) {
|
||||
return {
|
||||
notes: initialNotes,
|
||||
showModal: false,
|
||||
editing: null,
|
||||
@push('scripts')
|
||||
<script>
|
||||
function notesApp(initialNotes) {
|
||||
return {
|
||||
notes: initialNotes,
|
||||
showModal: false,
|
||||
editing: null,
|
||||
|
||||
// editor state
|
||||
editorTitle: '',
|
||||
editorBlocks: [],
|
||||
editorColor: '#FFFFFF',
|
||||
editorPinned: false,
|
||||
saving: false,
|
||||
nextBlockId: 1,
|
||||
searchQuery: '',
|
||||
// editor state
|
||||
editorTitle: '',
|
||||
editorBlocks: [],
|
||||
editorColor: '#FFFFFF',
|
||||
editorPinned: false,
|
||||
saving: false,
|
||||
nextBlockId: 1,
|
||||
searchQuery: '',
|
||||
|
||||
noteColors: [
|
||||
'#FFFFFF', '#FFE4E8', '#FFF9C4', '#E8F5E9',
|
||||
'#E3F2FD', '#F3E5F5', '#FFF3E0', '#E0F7FA'
|
||||
],
|
||||
noteColors: [
|
||||
'#FFFFFF', '#FFE4E8', '#FFF9C4', '#E8F5E9',
|
||||
'#E3F2FD', '#F3E5F5', '#FFF3E0', '#E0F7FA'
|
||||
],
|
||||
|
||||
// ── Computed / Filtered ───────────────────────────────
|
||||
get filteredPinned() {
|
||||
return this.filterNotes(this.notes.filter(n => n.is_pinned));
|
||||
},
|
||||
|
||||
get filteredUnpinned() {
|
||||
return this.filterNotes(this.notes.filter(n => !n.is_pinned));
|
||||
},
|
||||
|
||||
filterNotes(list) {
|
||||
const q = this.searchQuery.trim().toLowerCase();
|
||||
if (!q) return list;
|
||||
return list.filter(n =>
|
||||
(n.title || '').toLowerCase().includes(q) ||
|
||||
(n.content || []).some(b => (b.text || '').toLowerCase().includes(q))
|
||||
);
|
||||
},
|
||||
|
||||
// ── Modal open/close ─────────────────────────────────
|
||||
openNew() {
|
||||
this.editing = null;
|
||||
this.editorTitle = '';
|
||||
this.editorBlocks = [this.makeBlock('paragraph')];
|
||||
this.editorColor = '#FFFFFF';
|
||||
this.editorPinned = false;
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => this.$refs.titleInput?.focus());
|
||||
},
|
||||
|
||||
openEdit(note) {
|
||||
this.editing = note;
|
||||
this.editorTitle = note.title || '';
|
||||
this.editorBlocks = (note.content && note.content.length)
|
||||
? note.content.map(b => ({ ...b, id: this.nextBlockId++ }))
|
||||
: [this.makeBlock('paragraph')];
|
||||
this.editorColor = note.color || '#FFFFFF';
|
||||
this.editorPinned = !!note.is_pinned;
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => this.$refs.titleInput?.focus());
|
||||
},
|
||||
|
||||
// ── CRUD ─────────────────────────────────────────────
|
||||
async saveNote() {
|
||||
if (!this.editorTitle.trim()) {
|
||||
this.$refs.titleInput?.focus();
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
|
||||
const payload = {
|
||||
title: this.editorTitle.trim(),
|
||||
content: this.editorBlocks.map(({ type, text, checked }) => ({ type, text: text || '', checked: !!checked })),
|
||||
color: this.editorColor,
|
||||
is_pinned: this.editorPinned,
|
||||
};
|
||||
|
||||
const url = this.editing ? `/notes/${this.editing.id}` : '/notes';
|
||||
const method = this.editing ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
// ── Computed / Filtered ───────────────────────────────
|
||||
get filteredPinned() {
|
||||
return this.filterNotes(this.notes.filter(n => n.is_pinned));
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const saved = await res.json();
|
||||
|
||||
if (this.editing) {
|
||||
const idx = this.notes.findIndex(n => n.id === this.editing.id);
|
||||
if (idx !== -1) this.notes.splice(idx, 1, saved);
|
||||
} else {
|
||||
this.notes.unshift(saved);
|
||||
}
|
||||
get filteredUnpinned() {
|
||||
return this.filterNotes(this.notes.filter(n => !n.is_pinned));
|
||||
},
|
||||
|
||||
this.showModal = false;
|
||||
} catch (err) {
|
||||
console.error('Save failed', err);
|
||||
filterNotes(list) {
|
||||
const q = this.searchQuery.trim().toLowerCase();
|
||||
if (!q) return list;
|
||||
return list.filter(n =>
|
||||
(n.title || '').toLowerCase().includes(q) ||
|
||||
(n.content || []).some(b => (b.text || '').toLowerCase().includes(q))
|
||||
);
|
||||
},
|
||||
|
||||
// ── Modal open/close ─────────────────────────────────
|
||||
openNew() {
|
||||
this.editing = null;
|
||||
this.editorTitle = '';
|
||||
this.editorBlocks = [this.makeBlock('paragraph')];
|
||||
this.editorColor = '#FFFFFF';
|
||||
this.editorPinned = false;
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => this.$refs.titleInput?.focus());
|
||||
},
|
||||
|
||||
openEdit(note) {
|
||||
this.editing = note;
|
||||
this.editorTitle = note.title || '';
|
||||
this.editorBlocks = (note.content && note.content.length)
|
||||
? note.content.map(b => ({ ...b, id: this.nextBlockId++ }))
|
||||
: [this.makeBlock('paragraph')];
|
||||
this.editorColor = note.color || '#FFFFFF';
|
||||
this.editorPinned = !!note.is_pinned;
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => this.$refs.titleInput?.focus());
|
||||
},
|
||||
|
||||
// ── CRUD ─────────────────────────────────────────────
|
||||
async saveNote() {
|
||||
if (!this.editorTitle.trim()) {
|
||||
this.$refs.titleInput?.focus();
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
|
||||
const payload = {
|
||||
title: this.editorTitle.trim(),
|
||||
content: this.editorBlocks.map(({ type, text, checked }) => ({ type, text: text || '', checked: !!checked })),
|
||||
color: this.editorColor,
|
||||
is_pinned: this.editorPinned,
|
||||
};
|
||||
|
||||
const url = this.editing ? `/notes/${this.editing.id}` : '/notes';
|
||||
const method = this.editing ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const saved = await res.json();
|
||||
|
||||
if (this.editing) {
|
||||
const idx = this.notes.findIndex(n => n.id === this.editing.id);
|
||||
if (idx !== -1) this.notes.splice(idx, 1, saved);
|
||||
} else {
|
||||
this.notes.unshift(saved);
|
||||
}
|
||||
|
||||
this.showModal = false;
|
||||
} catch (err) {
|
||||
console.error('Save failed', err);
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
async deleteNote(note) {
|
||||
if (!confirm(`Delete "${note.title || 'this note'}"?`)) return;
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
|
||||
const res = await fetch(`/notes/${note.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) {
|
||||
this.notes = this.notes.filter(n => n.id !== note.id);
|
||||
if (this.editing && this.editing.id === note.id) {
|
||||
this.showModal = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async togglePin(note) {
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
const res = await fetch(`/notes/${note.id}/pin`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
const idx = this.notes.findIndex(n => n.id === note.id);
|
||||
if (idx !== -1) this.notes.splice(idx, 1, updated);
|
||||
}
|
||||
},
|
||||
|
||||
// ── Block editor helpers ──────────────────────────────
|
||||
makeBlock(type = 'paragraph') {
|
||||
return { id: this.nextBlockId++, type, text: '', checked: false };
|
||||
},
|
||||
|
||||
insertBlockAfter(idx) {
|
||||
const currentType = this.editorBlocks[idx]?.type ?? 'paragraph';
|
||||
const newBlock = this.makeBlock(currentType);
|
||||
this.editorBlocks.splice(idx + 1, 0, newBlock);
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(`[data-block-id="${newBlock.id}"]`)?.focus();
|
||||
});
|
||||
},
|
||||
|
||||
handleBackspace(e, block, idx) {
|
||||
if (block.text === '' && this.editorBlocks.length > 1) {
|
||||
e.preventDefault();
|
||||
this.editorBlocks.splice(idx, 1);
|
||||
this.$nextTick(() => {
|
||||
const target = this.editorBlocks[Math.max(0, idx - 1)];
|
||||
if (target) document.querySelector(`[data-block-id="${target.id}"]`)?.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addBlock(type) {
|
||||
const nb = this.makeBlock(type);
|
||||
this.editorBlocks.push(nb);
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(`[data-block-id="${nb.id}"]`)?.focus();
|
||||
});
|
||||
},
|
||||
|
||||
removeBlock(idx) {
|
||||
if (this.editorBlocks.length > 1) {
|
||||
this.editorBlocks.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
changeType(idx, type) {
|
||||
this.editorBlocks[idx].type = type;
|
||||
},
|
||||
|
||||
focusFirstBlock() {
|
||||
const first = this.editorBlocks[0];
|
||||
if (first) {
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(`[data-block-id="${first.id}"]`)?.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────
|
||||
formatDate(dt) {
|
||||
if (!dt) return '';
|
||||
const d = new Date(dt);
|
||||
const now = new Date();
|
||||
const diff = now - d;
|
||||
|
||||
if (diff < 60000) return 'just now';
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
||||
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
|
||||
|
||||
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' });
|
||||
},
|
||||
};
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
async deleteNote(note) {
|
||||
if (!confirm(`Delete "${note.title || 'this note'}"?`)) return;
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
|
||||
const res = await fetch(`/notes/${note.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) {
|
||||
this.notes = this.notes.filter(n => n.id !== note.id);
|
||||
if (this.editing && this.editing.id === note.id) {
|
||||
this.showModal = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async togglePin(note) {
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
const res = await fetch(`/notes/${note.id}/pin`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
const idx = this.notes.findIndex(n => n.id === note.id);
|
||||
if (idx !== -1) this.notes.splice(idx, 1, updated);
|
||||
}
|
||||
},
|
||||
|
||||
// ── Block editor helpers ──────────────────────────────
|
||||
makeBlock(type = 'paragraph') {
|
||||
return { id: this.nextBlockId++, type, text: '', checked: false };
|
||||
},
|
||||
|
||||
insertBlockAfter(idx) {
|
||||
const currentType = this.editorBlocks[idx]?.type ?? 'paragraph';
|
||||
const newBlock = this.makeBlock(currentType);
|
||||
this.editorBlocks.splice(idx + 1, 0, newBlock);
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(`[data-block-id="${newBlock.id}"]`)?.focus();
|
||||
});
|
||||
},
|
||||
|
||||
handleBackspace(e, block, idx) {
|
||||
if (block.text === '' && this.editorBlocks.length > 1) {
|
||||
e.preventDefault();
|
||||
this.editorBlocks.splice(idx, 1);
|
||||
this.$nextTick(() => {
|
||||
const target = this.editorBlocks[Math.max(0, idx - 1)];
|
||||
if (target) document.querySelector(`[data-block-id="${target.id}"]`)?.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addBlock(type) {
|
||||
const nb = this.makeBlock(type);
|
||||
this.editorBlocks.push(nb);
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(`[data-block-id="${nb.id}"]`)?.focus();
|
||||
});
|
||||
},
|
||||
|
||||
removeBlock(idx) {
|
||||
if (this.editorBlocks.length > 1) {
|
||||
this.editorBlocks.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
changeType(idx, type) {
|
||||
this.editorBlocks[idx].type = type;
|
||||
},
|
||||
|
||||
focusFirstBlock() {
|
||||
const first = this.editorBlocks[0];
|
||||
if (first) {
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(`[data-block-id="${first.id}"]`)?.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────
|
||||
formatDate(dt) {
|
||||
if (!dt) return '';
|
||||
const d = new Date(dt);
|
||||
const now = new Date();
|
||||
const diff = now - d;
|
||||
|
||||
if (diff < 60000) return 'just now';
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
||||
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
|
||||
|
||||
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' });
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
Reference in New Issue
Block a user