Add PWA and mobile creation flow
This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
{% csrf_token %}
|
||||
<div class="card-body vstack gap-2">
|
||||
<div class="small text-muted">Typ: {{ item.get_kind_display }}</div>
|
||||
<div class="mobile-tag-picker">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">Tags hinzufügen</button>
|
||||
<div class="mobile-tag-backdrop" x-show="showTags" @click="showTags=false" x-cloak></div>
|
||||
<div class="mobile-tag-popup card shadow" x-show="showTags" x-cloak>
|
||||
<div class="card-body"><div class="d-flex justify-content-between align-items-center mb-2"><strong>Tag auswählen</strong><button type="button" class="btn-close" @click="showTags=false"></button></div><div class="d-flex flex-wrap gap-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="insert('#{{ tag.name|escapejs }}'); showTags=false">#{{ tag.name }}</button>{% endfor %}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-control" name="content" rows="5" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.content }}</textarea>
|
||||
<textarea class="form-control" name="comment" rows="3" placeholder="Kommentar, z.B. gleiche Aufgabe bereits erfasst [[12]]" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.comment }}</textarea>
|
||||
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% load markdown_extras %}
|
||||
<article id="item-{{ item.id }}" class="card item-card shadow-sm {% if item.is_done %}done{% endif %}" ondblclick="if(!event.target.closest('a,button,input,textarea,select,label')) window.location.href='{{ item.get_absolute_url }}?next={{ request.get_full_path|urlencode }}'">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<div class="flex-grow-1 min-w-0" style="min-width:0">
|
||||
<div class="item-row d-flex justify-content-between align-items-start gap-3">
|
||||
<div class="item-main flex-grow-1 min-w-0" style="min-width:0">
|
||||
<div class="mb-2 d-flex flex-wrap gap-2 align-items-center">
|
||||
<span class="badge {% if item.kind == 'todo' %}text-bg-primary{% elif item.kind == 'link' %}text-bg-success{% elif item.kind == 'journal' %}text-bg-info{% else %}text-bg-secondary{% endif %}">{{ item.get_kind_display }}</span>
|
||||
<span class="badge text-bg-light">{{ item.get_visibility_display }}</span>
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<div class="item-actions btn-group btn-group-sm">
|
||||
{% if item.kind == 'todo' %}<button class="btn btn-outline-primary" hx-post="{% url 'toggle_done' item.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML">✓</button>{% endif %}
|
||||
{% if request.resolver_match.url_name == 'item_detail' %}
|
||||
<a class="btn btn-outline-secondary" href="{% url 'item_editor' item.id %}?next={{ request.GET.next|default:'/'|urlencode }}">✎</a>
|
||||
|
||||
@@ -5,22 +5,29 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>my2dos</title>
|
||||
<meta name="theme-color" content="#212529">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="my2dos">
|
||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||||
<link rel="icon" href="{% static 'icon.svg' %}" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="{% static 'icon.svg' %}">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>
|
||||
body{background:#f7f7f4}.shell{max-width:960px}.item-card{border:0;border-radius:1rem}.content p:last-child{margin-bottom:0}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}
|
||||
body{background:#f7f7f4}.shell{max-width:960px}.item-card{border:0;border-radius:1rem}.content p:last-child{margin-bottom:0}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media,.mobile-tag-picker{display:none}.mobile-tag-popup{position:fixed;inset:auto .75rem 1rem .75rem;z-index:1050;max-height:55vh;overflow:auto}.mobile-tag-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1040}.camera-input{max-width:13rem}@media (max-width: 767.98px){.shell{max-width:100%}main.container{padding-left:.75rem;padding-right:.75rem}.navbar .container{gap:.5rem}.navbar-brand{font-size:1rem}.navbar .d-flex{gap:.5rem!important}.item-card{border-radius:.75rem}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media{display:flex!important}.mobile-tag-picker{display:block!important}.desktop-quick-create{display:none!important}.mobile-create-nav{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.desktop-file{display:none!important}.mobile-create-buttons{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.quick-actions{flex-wrap:wrap}.quick-actions .form-control{min-width:100%}.slash-menu{width:calc(100vw - 1.5rem)}textarea.form-control-lg{font-size:1rem}.btn{touch-action:manipulation}.item-row{flex-direction:column}.item-actions{align-self:flex-end}.item-main{width:100%;min-width:0}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar bg-white border-bottom sticky-top"><div class="container shell"><a class="navbar-brand fw-bold" href="/">my2dos</a><div class="d-flex align-items-center gap-3"><a class="text-muted small d-none d-sm-inline text-decoration-none" href="/">Notizen · Aufgaben · Links</a>{% if user.is_authenticated %}<a class="small" href="{% url 'journal' %}">Journal</a><a class="small" href="{% url 'kanban_list' %}">Kanban</a><div class="dropdown"><button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">{{ user.get_full_name|default:user.username }}</button><ul class="dropdown-menu dropdown-menu-end"><li><a class="dropdown-item" href="{% url 'settings' %}">Einstellungen</a></li>{% if user.is_staff %}<li><a class="dropdown-item" href="/admin/">Admin</a></li>{% endif %}<li><hr class="dropdown-divider"></li><li><form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="dropdown-item">Logout</button></form></li></ul></div>{% else %}<a class="btn btn-sm btn-dark" href="{% url 'login' %}">Login</a>{% endif %}</div></div></nav>
|
||||
<nav class="navbar bg-white border-bottom sticky-top"><div class="container shell"><a class="navbar-brand fw-bold" href="/">my2dos</a><div class="d-flex align-items-center gap-3"><a class="small d-none d-sm-inline text-primary text-decoration-underline" href="/">Notizen · Aufgaben · Links</a>{% if user.is_authenticated %}<a class="small" href="{% url 'journal' %}">Journal</a><a class="small" href="{% url 'kanban_list' %}">Kanban</a><div class="dropdown"><button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">{{ user.get_full_name|default:user.username }}</button><ul class="dropdown-menu dropdown-menu-end"><li><a class="dropdown-item" href="{% url 'settings' %}">Einstellungen</a></li>{% if user.is_staff %}<li><a class="dropdown-item" href="/admin/">Admin</a></li>{% endif %}<li><hr class="dropdown-divider"></li><li><form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="dropdown-item">Logout</button></form></li></ul></div>{% else %}<a class="btn btn-sm btn-dark" href="{% url 'login' %}">Login</a>{% endif %}</div></div></nav>
|
||||
<main class="container shell py-4">{% for message in messages %}<div class="alert alert-{{ message.tags|default:'info' }}">{{ message }}</div>{% endfor %}{% block content %}{% endblock %}</main>
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', e => {e.detail.headers['X-CSRFToken']='{{ csrf_token }}'});
|
||||
window.quickComposer = window.quickComposer || function(tags, itemId=null){
|
||||
return {
|
||||
open:false,text:'',active:0,query:'',currentEl:null,preview:null,pendingFiles:[],linkSuggestions:[],attachmentSuggestions:[],itemId:itemId,tags:tags||[],
|
||||
open:false,showTags:false,text:'',active:0,query:'',currentEl:null,preview:null,pendingFiles:[],linkSuggestions:[],attachmentSuggestions:[],itemId:itemId,tags:tags||[],
|
||||
init(){document.body.addEventListener('tagsChanged',()=>{fetch('/api/tags/').then(r=>r.json()).then(d=>{this.tags=d.tags||[]})})},
|
||||
commands:[{token:'/todo',label:'Aufgabe erstellen'},{token:'/link',label:'Link speichern'},{token:'/journal',label:'Journal-Eintrag'},{token:'/public',label:'öffentlich'},{token:'/private',label:'privat'},{token:'/code',label:'Markdown Codeblock einfügen'},{token:'[[',label:'interne Aufgabe/Notiz suchen'},{token:'![[',label:'Bild aus Anhängen einfügen'}],
|
||||
get filtered(){let q=this.query.toLowerCase();let list=q.startsWith('#')?this.tags.filter(t=>('#'+t).toLowerCase().startsWith(q)).map(t=>({token:'#'+t,label:'Tag'})):q.startsWith('![[')?(this.itemId?this.attachmentSuggestions.map(a=>({token:`![[file:${a.id}]]`,label:`Bild: ${a.name}`})):this.pendingFiles.map(f=>({token:`![[paste:${f.name}]]`,label:`Bild: ${f.name}`}))):q.startsWith('[[')?this.linkSuggestions.map(i=>({token:`[[${i.id}]]`,label:`${i.kind}: ${i.label}`})):this.commands.filter(c=>!q||c.token.toLowerCase().startsWith(q));return list.length?list:[{token:this.query,label:'neu'}]},
|
||||
@@ -36,6 +43,7 @@ window.quickComposer = window.quickComposer || function(tags, itemId=null){
|
||||
window.searchBox = function(tags){return{open:false,active:0,q:'',tags:tags||[],get filtered(){let q=this.q.toLowerCase();return this.tags.filter(t=>('#'+t).toLowerCase().startsWith(q))},onInput(e){this.q=e.target.value;this.open=this.q.startsWith('#')},onKeydown(e){if(!this.open)return;if(e.key==='ArrowDown'){e.preventDefault();this.active=(this.active+1)%Math.max(this.filtered.length,1)}else if(e.key==='ArrowUp'){e.preventDefault();this.active=(this.active-1+Math.max(this.filtered.length,1))%Math.max(this.filtered.length,1)}else if(e.key==='Enter'||e.key==='Tab'){if(this.filtered.length){e.preventDefault();this.select(this.filtered[this.active])}}else if(e.key==='Escape'){this.open=false}},select(tag){let url=new URL(window.location.href);url.searchParams.set('tag',tag);url.searchParams.delete('q');window.location.href=url.pathname+'?'+url.searchParams.toString()}}}
|
||||
function enhanceCodeBlocks(root=document){root.querySelectorAll('.content pre:not([data-copy])').forEach(pre=>{pre.dataset.copy='1';let btn=document.createElement('button');btn.type='button';btn.className='copy-code btn btn-sm btn-light';btn.title='Kopieren';btn.innerHTML='⧉';btn.addEventListener('click',()=>navigator.clipboard.writeText(pre.querySelector('code')?.innerText || pre.innerText).then(()=>{btn.innerHTML='✓';setTimeout(()=>btn.innerHTML='⧉',1200)}));pre.appendChild(btn)})}
|
||||
document.addEventListener('DOMContentLoaded',()=>enhanceCodeBlocks());document.body.addEventListener('htmx:afterSwap',e=>enhanceCodeBlocks(e.target));
|
||||
if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('{% static "sw.js" %}').catch(()=>{}));}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load querystring %}
|
||||
{% block content %}
|
||||
<div class="mobile-create-nav gap-2 mb-3">
|
||||
<a class="btn btn-sm btn-primary flex-fill" href="{% url 'new_item' %}?kind=todo&next={{ request.get_full_path|urlencode }}">Aufgabe</a>
|
||||
<a class="btn btn-sm btn-secondary flex-fill" href="{% url 'new_item' %}?kind=note&next={{ request.get_full_path|urlencode }}">Notiz</a>
|
||||
<a class="btn btn-sm btn-success flex-fill" href="{% url 'new_item' %}?kind=link&next={{ request.get_full_path|urlencode }}">Link</a>
|
||||
<a class="btn btn-sm btn-info flex-fill" href="{% url 'new_item' %}?kind=journal&next={{ request.get_full_path|urlencode }}">Journal</a>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<aside class="col-lg-3 order-lg-2">
|
||||
<div class="card item-card shadow-sm"><div class="card-body">
|
||||
@@ -31,14 +37,22 @@
|
||||
</div></div>
|
||||
</aside>
|
||||
<section class="col-lg-9">
|
||||
<form class="position-relative mb-4" method="post" enctype="multipart/form-data" hx-post="{{ request.get_full_path }}" hx-target="#items" hx-swap="afterbegin" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:htmx:after-request="if($event.detail.successful){text='';open=false;$el.reset()}">
|
||||
<form class="desktop-quick-create position-relative mb-4" method="post" enctype="multipart/form-data" hx-post="{{ request.get_full_path }}" hx-target="#items" hx-swap="afterbegin" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:htmx:after-request="if($event.detail.successful){text='';open=false;$el.reset()}">
|
||||
{% csrf_token %}
|
||||
<div class="mobile-create-buttons gap-2 mb-2">
|
||||
<button type="button" class="btn btn-sm btn-primary flex-fill" @click="insert('/todo')">Aufgabe</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary flex-fill" @click="$el.querySelector('textarea').focus()">Notiz</button>
|
||||
<button type="button" class="btn btn-sm btn-success flex-fill" @click="insert('/link')">Link</button>
|
||||
<button type="button" class="btn btn-sm btn-info flex-fill" @click="insert('/journal')">Journal</button>
|
||||
</div>
|
||||
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">Tags hinzufügen</summary><div class="mobile-tags flex-wrap gap-2 mt-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="insert('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
|
||||
{{ form.content }}
|
||||
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
|
||||
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></template>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 mt-2">
|
||||
<input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)">
|
||||
<div class="d-flex align-items-center gap-2 mt-2 quick-actions">
|
||||
<input class="form-control desktop-file" type="file" name="files" multiple accept="image/*,.pdf,.txt,.md" x-ref="fileInput" @change="previewFile($event)">
|
||||
<div class="mobile-media gap-2 w-100"><label class="btn btn-outline-secondary flex-fill mb-0">Galerie<input class="d-none" type="file" name="files" multiple accept="image/*" @change="previewFile($event)"></label><label class="btn btn-outline-secondary flex-fill mb-0">Foto<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div>
|
||||
<a class="btn btn-outline-secondary" href="{% url 'new_item' %}">Editor</a>
|
||||
<button class="btn btn-dark px-4">Speichern</button>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
<div class="card-body vstack gap-3">
|
||||
<h1 class="h4">Eintrag #{{ item.id }} im Editor bearbeiten</h1>
|
||||
<div class="small text-muted">Typ: {{ item.get_kind_display }}</div>
|
||||
<div class="mobile-tag-picker">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">Tags hinzufügen</button>
|
||||
<div class="mobile-tag-backdrop" x-show="showTags" @click="showTags=false" x-cloak></div>
|
||||
<div class="mobile-tag-popup card shadow" x-show="showTags" x-cloak>
|
||||
<div class="card-body"><div class="d-flex justify-content-between align-items-center mb-2"><strong>Tag auswählen</strong><button type="button" class="btn-close" @click="showTags=false"></button></div><div class="d-flex flex-wrap gap-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="insert('#{{ tag.name|escapejs }}'); showTags=false">#{{ tag.name }}</button>{% endfor %}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-control" name="content" rows="18" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.content }}</textarea>
|
||||
<textarea class="form-control" name="comment" rows="5" placeholder="Kommentar" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.comment }}</textarea>
|
||||
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
|
||||
|
||||
@@ -20,9 +20,16 @@
|
||||
<h1 class="h4 mb-3">Journal {{ selected_day }}</h1>
|
||||
<form class="position-relative mb-4" method="post" enctype="multipart/form-data" hx-post="{{ request.get_full_path }}" hx-target="#items" hx-swap="afterbegin" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:htmx:after-request="if($event.detail.successful){text='';open=false;$el.reset()}">
|
||||
{% csrf_token %}
|
||||
<div class="mobile-create-buttons gap-2 mb-2">
|
||||
<button type="button" class="btn btn-sm btn-info flex-fill" @click="insert('/journal')">Journal</button>
|
||||
<button type="button" class="btn btn-sm btn-primary flex-fill" @click="insert('/todo')">Aufgabe</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary flex-fill" @click="$el.querySelector('textarea').focus()">Notiz</button>
|
||||
<button type="button" class="btn btn-sm btn-success flex-fill" @click="insert('/link')">Link</button>
|
||||
</div>
|
||||
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">Tags hinzufügen</summary><div class="mobile-tags flex-wrap gap-2 mt-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="insert('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
|
||||
{{ form.content }}
|
||||
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak><template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></template></div>
|
||||
<div class="d-flex align-items-center gap-2 mt-2"><input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)"><button class="btn btn-dark px-4">Speichern</button></div>
|
||||
<div class="d-flex align-items-center gap-2 mt-2 quick-actions"><input class="form-control desktop-file" type="file" name="files" multiple accept="image/*,.pdf,.txt,.md" x-ref="fileInput" @change="previewFile($event)"><div class="mobile-media gap-2 w-100"><label class="btn btn-outline-secondary flex-fill mb-0">Galerie<input class="d-none" type="file" name="files" multiple accept="image/*" @change="previewFile($event)"></label><label class="btn btn-outline-secondary flex-fill mb-0">Foto<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div><button class="btn btn-dark px-4">Speichern</button></div>
|
||||
</form>
|
||||
<div id="items" class="vstack gap-3">
|
||||
{% for item in items %}{% include 'core/_item.html' %}{% empty %}<div class="text-center text-muted py-5">Keine Journal-Aktivität an diesem Tag.</div>{% endfor %}
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% block content %}
|
||||
<a class="btn btn-sm btn-outline-secondary mb-3" href="/">← zurück</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-3" href="{{ next_url }}">← zurück</a>
|
||||
<form class="card item-card shadow-sm position-relative" method="post" enctype="multipart/form-data" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next_url }}">
|
||||
<div class="card-body vstack gap-3">
|
||||
<h1 class="h4">Erweiterter Editor</h1>
|
||||
<textarea class="form-control" name="content" rows="16" placeholder="Längeres Dokument … /todo /link /journal /public #tag [[ ![[" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)"></textarea>
|
||||
<h1 class="h4">{% if requested_kind == 'todo' %}Aufgabe erstellen{% elif requested_kind == 'link' %}Link erstellen{% elif requested_kind == 'journal' %}Journal-Eintrag erstellen{% else %}Notiz erstellen{% endif %}</h1>
|
||||
<div class="mobile-tag-picker">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">Tags hinzufügen</button>
|
||||
<div class="mobile-tag-backdrop" x-show="showTags" @click="showTags=false" x-cloak></div>
|
||||
<div class="mobile-tag-popup card shadow" x-show="showTags" x-cloak>
|
||||
<div class="card-body"><div class="d-flex justify-content-between align-items-center mb-2"><strong>Tag auswählen</strong><button type="button" class="btn-close" @click="showTags=false"></button></div><div class="d-flex flex-wrap gap-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="insert('#{{ tag.name|escapejs }}'); showTags=false">#{{ tag.name }}</button>{% endfor %}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-control" name="content" rows="14" placeholder="Text … #tag [[ ![[" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)"></textarea>
|
||||
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
|
||||
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></template>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Bilder/Dateien</label>
|
||||
<input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)">
|
||||
<div class="form-text">Mit <code>![[</code> können hochgeladene oder gepastete Bilder im Text eingefügt werden.</div>
|
||||
<label class="form-label">Bild hinzufügen</label>
|
||||
<input class="form-control desktop-file" type="file" name="files" multiple accept="image/*,.pdf,.txt,.md" x-ref="fileInput" @change="previewFile($event)">
|
||||
<div class="mobile-media gap-2"><label class="btn btn-outline-secondary flex-fill mb-0">Galerie<input class="d-none" type="file" name="files" multiple accept="image/*" @change="previewFile($event)"></label><label class="btn btn-outline-secondary flex-fill mb-0">Foto<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div>
|
||||
<div class="form-text">Mit <code>![[</code> können Bilder im Text eingefügt werden.</div>
|
||||
</div>
|
||||
<template x-if="preview"><div class="small"><span class="text-muted">Bildvorschau:</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:180px"></div></template>
|
||||
<button class="btn btn-dark align-self-start">Speichern</button>
|
||||
|
||||
+10
-3
@@ -162,15 +162,22 @@ def index(request):
|
||||
|
||||
@login_required
|
||||
def new_item(request):
|
||||
requested_kind = request.GET.get('kind', '')
|
||||
next_url = request.GET.get('next') or request.POST.get('next') or 'index'
|
||||
if request.method == 'POST':
|
||||
form = QuickItemForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
kind, visibility, content, url = parse_quick_content(form.cleaned_data['content'])
|
||||
raw = form.cleaned_data['content']
|
||||
if requested_kind in {'todo', 'link', 'journal'} and not re.search(r'/(todo|note|link|journal)\b', raw, re.I):
|
||||
raw = f'/{requested_kind} {raw}'
|
||||
kind, visibility, content, url = parse_quick_content(raw)
|
||||
item = Item.objects.create(owner=request.user, kind=kind, visibility=visibility, content=content, url=url)
|
||||
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
|
||||
item.sync_metadata()
|
||||
return redirect(item.get_absolute_url())
|
||||
return render(request, 'core/new_item.html', {'form': QuickItemForm(), 'tags': Tag.objects.all()})
|
||||
return redirect(next_url)
|
||||
return render(request, 'core/new_item.html', {
|
||||
'form': QuickItemForm(), 'tags': Tag.objects.all(), 'requested_kind': requested_kind, 'next_url': next_url,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user