Add teams and internationalization

This commit is contained in:
rucki
2026-08-23 14:55:04 +02:00
parent 218d841d7f
commit c409fbc441
26 changed files with 1582 additions and 149 deletions
+12 -11
View File
@@ -1,33 +1,34 @@
{% load i18n %}
<form id="item-{{ item.id }}" class="card item-card shadow-sm position-relative" method="post" enctype="multipart/form-data" hx-post="{% url 'edit_item' item.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}], {{ item.id }})" x-init="text=$el.querySelector('textarea').value">
{% csrf_token %}
<div class="card-body vstack gap-2">
<div class="small text-muted">Typ: {{ item.get_kind_display }}</div>
<div class="small text-muted">{% trans "Type:" %} {{ 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>
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">{% trans "Add tags" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></div>
<div class="card-body"><div class="d-flex justify-content-between align-items-center mb-2"><strong>{% trans "Choose tag" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ 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>
<textarea class="form-control" name="comment" rows="3" placeholder="{% trans 'Comment, e.g. same task already captured [[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>
<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="row g-2">
<div class="col-md-4"><select class="form-select" name="visibility">
<option value="private" {% if item.visibility == 'private' %}selected{% endif %}>Privat</option>
<option value="public" {% if item.visibility == 'public' %}selected{% endif %}>Öffentlich</option>
<option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option>
<option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option>
</select></div>
{% if item.kind == 'todo' %}<div class="col-md-4"><input class="form-control" type="datetime-local" name="due_at" value="{{ item.due_at|date:'Y-m-d\\TH:i' }}" title="Datum/Zeit für Todo"></div>{% endif %}
{% if item.kind == 'todo' %}<div class="col-md-4"><input class="form-control" type="datetime-local" name="due_at" value="{{ item.due_at|date:'Y-m-d\\TH:i' }}" title="{% trans 'Date/time for todo' %}"></div>{% endif %}
<div class="col-md-4"><input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)"></div>
</div>
{% if item.kind == 'link' %}<input class="form-control" type="url" name="url" value="{{ item.url }}" placeholder="URL">{% endif %}
<template x-if="preview"><div class="small"><span class="text-muted">Bild aus Zwischenablage:</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:160px"></div></template>
<template x-if="preview"><div class="small"><span class="text-muted">{% trans "Image from clipboard:" %}</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:160px"></div></template>
<div class="d-flex gap-2">
<button class="btn btn-dark btn-sm">Speichern</button>
<a class="btn btn-outline-primary btn-sm" href="{% url 'item_editor' item.id %}?next={{ request.get_full_path|urlencode }}">Im Editor öffnen</a>
<button type="button" class="btn btn-outline-secondary btn-sm" hx-get="{% url 'item_card' item.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML">Abbrechen</button>
<button class="btn btn-dark btn-sm">{% trans "Save" %}</button>
<a class="btn btn-outline-primary btn-sm" href="{% url 'item_editor' item.id %}?next={{ request.get_full_path|urlencode }}">{% trans "Open in editor" %}</a>
<button type="button" class="btn btn-outline-secondary btn-sm" hx-get="{% url 'item_card' item.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML">{% trans "Cancel" %}</button>
</div>
</div>
</form>
+6 -6
View File
@@ -1,24 +1,24 @@
{% load markdown_extras %}
{% load markdown_extras i18n %}
<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="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>
{% if item.team %}<a class="badge text-bg-warning text-decoration-none" href="{% url 'team_detail' item.team.id %}">{{ item.team.name }}</a>{% else %}<span class="badge text-bg-light">{{ item.get_visibility_display }}</span>{% endif %}
<a class="text-muted small" href="{{ item.get_absolute_url }}?next={{ request.get_full_path|urlencode }}">#{{ item.id }}</a>
{% for tag in item.tags.all %}<a class="badge rounded-pill text-bg-light text-decoration-none tag" href="/?tag={{ tag.name }}">#{{ tag.name }}</a>{% endfor %}
</div>
<div x-data="{expanded:false,showMore:false,check(){this.$nextTick(()=>{let el=this.$refs.content;this.showMore=el && el.scrollHeight > el.clientHeight + 2})}}" x-init="check()" class="position-relative">
<div x-ref="content" class="content overview-content" :class="expanded ? 'expanded' : ''" style="--overview-lines: {{ overview_lines|default:999 }}">{{ item|item_markdown }}</div>
{% if overview_lines %}<button x-show="showMore" type="button" class="btn btn-link btn-sm p-0 mt-1" @click="expanded=!expanded" x-text="expanded ? 'weniger anzeigen' : 'mehr anzeigen'"></button>{% endif %}
{% if overview_lines %}<button x-show="showMore" type="button" class="btn btn-link btn-sm p-0 mt-1" @click="expanded=!expanded" x-text="expanded ? '{% trans "show less" %}' : '{% trans "show more" %}'"></button>{% endif %}
</div>
{% if item.comment %}<div class="border-start ps-2 mt-2 small content text-muted">{{ item.comment|markdown }}</div>{% endif %}
{% if item.due_at %}<div class="small text-warning-emphasis mt-2">Fällig: {{ item.due_at|date:'d.m.Y H:i' }}</div>{% endif %}
{% if item.due_at %}<div class="small text-warning-emphasis mt-2">{% trans "Due:" %} {{ item.due_at|date:'d.m.Y H:i' }}</div>{% endif %}
{% if item.url %}<a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.url }}</a>{% endif %}
{% if item.linked_items.all %}<div class="small mt-2">Links: {% for linked in item.linked_items.all %}<a href="{{ linked.get_absolute_url }}">#{{ linked.id }}</a> {% endfor %}</div>{% endif %}
{% if item.linked_items.all %}<div class="small mt-2">{% trans "Links:" %} {% for linked in item.linked_items.all %}<a href="{{ linked.get_absolute_url }}">#{{ linked.id }}</a> {% endfor %}</div>{% endif %}
{% if item.attachments.all %}
<div class="small mt-2">Anhänge: {% for a in item.attachments.all %}<a href="{{ a.file.url }}">{{ a.file.name }}</a> {% endfor %}</div>
<div class="small mt-2">{% trans "Attachments:" %} {% for a in item.attachments.all %}<a href="{{ a.file.url }}">{{ a.file.name }}</a> {% endfor %}</div>
<div class="d-flex flex-wrap gap-2 mt-2">
{% for a in item.attachments.all %}{% if a.file.name|is_image and not a|is_embedded:item %}<a href="{{ a.file.url }}" target="_blank"><img src="{{ a.file.url }}" class="img-thumbnail" style="max-width:220px;max-height:180px" alt="{{ a.file.name }}"></a>{% endif %}{% endfor %}
</div>
+2 -2
View File
@@ -1,4 +1,4 @@
{% load querystring %}
{% load querystring i18n %}
<div id="tag-list" class="d-flex flex-wrap gap-2"{% if oob %} hx-swap-oob="true"{% endif %}>
{% for tag in tags %}
{% if active_tag == tag.name %}
@@ -6,5 +6,5 @@
{% else %}
<a class="badge rounded-pill text-bg-light text-decoration-none tag" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>
{% endif %}
{% empty %}<span class="text-muted small">Noch keine Tags</span>{% endfor %}
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div>
+8 -7
View File
@@ -1,4 +1,4 @@
{% load static %}
{% load static i18n %}
<!doctype html>
<html lang="de">
<head>
@@ -21,7 +21,7 @@
</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="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><li><button id="pwa-install" type="button" class="dropdown-item d-none">App installieren</button></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="/">{% trans "Notes · Tasks · Links" %}</a>{% if user.is_authenticated %}<a class="small" href="{% url 'journal' %}">{% trans "Journal" %}</a><a class="small" href="{% url 'kanban_list' %}">{% trans "Kanban" %}</a><a class="small" href="{% url 'settings' %}#teams">{% trans "Teams" %}</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' %}">{% trans "Settings" %}</a></li><li><button id="pwa-install" type="button" class="dropdown-item d-none">{% trans "Install app" %}</button></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">{% trans "Logout" %}</button></form></li></ul></div>{% else %}<a class="btn btn-sm btn-dark" href="{% url 'login' %}">{% trans "Login" %}</a>{% endif %}<form action="{% url 'set_language' %}" method="post" class="d-flex align-items-center">{% csrf_token %}<input name="next" type="hidden" value="{{ request.get_full_path }}"><select name="language" class="form-select form-select-sm" onchange="this.form.submit()">{% get_current_language as LANGUAGE_CODE %}{% get_available_languages as LANGUAGES %}{% for code, name in LANGUAGES %}<option value="{{ code }}"{% if code == LANGUAGE_CODE %} selected{% endif %}>{{ name }}</option>{% endfor %}</select></form></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 }}'});
@@ -29,16 +29,17 @@ window.quickComposer = window.quickComposer || function(tags, itemId=null){
return {
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'}]},
commands:[{token:'/todo',label:'{% trans "Create task" %}'},{token:'/link',label:'{% trans "Save link" %}'},{token:'/journal',label:'{% trans "Journal entry" %}'},{token:'/public',label:'{% trans "public" %}'},{token:'/private',label:'{% trans "private" %}'},{token:'/code',label:'{% trans "Insert Markdown code block" %}'},{token:'[[',label:'{% trans "Search internal task/note" %}'},{token:'![[',label:'{% trans "Insert image from attachments" %}'}],
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:`{% trans "Image:" %} ${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'}]},
isContentField(el){return el && (el.name==='content' || el.id==='id_content')},
onInput(e){this.update(e.target)},
handlePaste(e){let item=[...(e.clipboardData?.items||[])].find(i=>i.kind==='file'&&i.type.startsWith('image/'));if(!item||!this.$refs.fileInput)return;let file=item.getAsFile();let ext=(file.type.split('/')[1]||'png').replace('jpeg','jpg');let named=new File([file],`paste-${new Date().toISOString().replace(/[:.]/g,'-')}.${ext}`,{type:file.type});let dt=new DataTransfer();[...(this.$refs.fileInput.files||[])].forEach(f=>dt.items.add(f));dt.items.add(named);this.$refs.fileInput.files=dt.files;this.pendingFiles=[...dt.files].filter(f=>f.type.startsWith('image/'));this.preview=URL.createObjectURL(named)},
insertImageToken(){if(this.pendingFiles.length)this.insert(`![[paste:${this.pendingFiles[this.pendingFiles.length-1].name}]]`)},
previewFile(e){this.pendingFiles=[...e.target.files].filter(f=>f.type.startsWith('image/'));let file=this.pendingFiles[0];this.preview=file?URL.createObjectURL(file):null},
onKeydown(e){this.currentEl=e.target;if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();this.open=false;e.target.form.requestSubmit();return}if(this.open&&e.key==='ArrowDown'){e.preventDefault();this.active=(this.active+1)%this.filtered.length;return}if(this.open&&e.key==='ArrowUp'){e.preventDefault();this.active=(this.active-1+this.filtered.length)%this.filtered.length;return}if(this.open&&(e.key==='Enter'||e.key==='Tab')&&!e.shiftKey){e.preventDefault();this.insert(this.filtered[this.active].token);return}if(e.key==='Escape'){this.open=false;return}if(e.key===' '){this.open=false;return}setTimeout(()=>this.update(e.target),0)},
update(el){this.currentEl=el;this.text=el.value;let before=el.value.slice(0,el.selectionStart);let fragment=(before.match(/(^|\s)([^\s]*)$/)||['','',''])[2];this.query=fragment;this.active=0;this.open=fragment.startsWith('/')||fragment.startsWith('#')||fragment.startsWith('[[')||fragment.startsWith('![[');if(fragment.startsWith('![[')&&this.itemId){fetch(`/api/attachment-suggestions/?item=${this.itemId}`).then(r=>r.json()).then(d=>{this.attachmentSuggestions=d.attachments||[]})}else if(fragment.startsWith('[[')){fetch(`/api/item-suggestions/?q=${encodeURIComponent(fragment.slice(2))}`).then(r=>r.json()).then(d=>{this.linkSuggestions=d.items||[]})}},
addToken(token){let el=this.currentEl||this.$el.querySelector('textarea');if(!el)return;let pos=el.selectionStart??el.value.length;let before=el.value.slice(0,pos);let after=el.value.slice(pos);let prefix=before&& !before.endsWith(' ') && !before.endsWith('\n')?' ':'';let value=before+prefix+token+' '+after;this.text=value;el.value=value;this.showTags=false;this.$nextTick(()=>{let p=before.length+prefix.length+token.length+1;el.focus();el.setSelectionRange(p,p)})},
insert(token){let el=this.currentEl||this.$el.querySelector('textarea');if(!el)return;let isCode=token==='/code';if(isCode)token='```\n\n```';let pos=el.selectionStart;let before=el.value.slice(0,pos);let after=el.value.slice(pos);let match=before.match(/(^|\s)([^\s]*)$/);let start=match?pos-match[2].length:pos;let needsPrefix=start>0&&el.value[start-1]!=='\n'&&el.value[start-1]!==' ';let prefix=isCode?(start>0&&el.value[start-1]!=='\n'?'\n':''):(needsPrefix?' ':'');let suffix=isCode?'': ' ';let value=el.value.slice(0,start)+prefix+token+suffix+after;this.text=value;el.value=value;this.open=false;this.$nextTick(()=>{let p=isCode?start+prefix.length+4:start+prefix.length+token.length+suffix.length;el.focus();el.setSelectionRange(p,p)})}
update(el){this.currentEl=el;if(this.isContentField(el))this.text=el.value;let before=el.value.slice(0,el.selectionStart);let fragment=(before.match(/(^|\s)([^\s]*)$/)||['','',''])[2];this.query=fragment;this.active=0;this.open=fragment.startsWith('/')||fragment.startsWith('#')||fragment.startsWith('[[')||fragment.startsWith('![[');if(fragment.startsWith('![[')&&this.itemId){fetch(`/api/attachment-suggestions/?item=${this.itemId}`).then(r=>r.json()).then(d=>{this.attachmentSuggestions=d.attachments||[]})}else if(fragment.startsWith('[[')){fetch(`/api/item-suggestions/?q=${encodeURIComponent(fragment.slice(2))}`).then(r=>r.json()).then(d=>{this.linkSuggestions=d.items||[]})}},
addToken(token){let el=this.currentEl||this.$el.querySelector('textarea');if(!el)return;let pos=el.selectionStart??el.value.length;let before=el.value.slice(0,pos);let after=el.value.slice(pos);let prefix=before&& !before.endsWith(' ') && !before.endsWith('\n')?' ':'';let value=before+prefix+token+' '+after;if(this.isContentField(el))this.text=value;el.value=value;this.showTags=false;this.$nextTick(()=>{let p=before.length+prefix.length+token.length+1;el.focus();el.setSelectionRange(p,p)})},
insert(token){let el=this.currentEl||this.$el.querySelector('textarea');if(!el)return;let isCode=token==='/code';if(isCode)token='```\n\n```';let pos=el.selectionStart;let before=el.value.slice(0,pos);let after=el.value.slice(pos);let match=before.match(/(^|\s)([^\s]*)$/);let start=match?pos-match[2].length:pos;let needsPrefix=start>0&&el.value[start-1]!=='\n'&&el.value[start-1]!==' ';let prefix=isCode?(start>0&&el.value[start-1]!=='\n'?'\n':''):(needsPrefix?' ':'');let suffix=isCode?'': ' ';let value=el.value.slice(0,start)+prefix+token+suffix+after;if(this.isContentField(el))this.text=value;el.value=value;this.open=false;this.$nextTick(()=>{let p=isCode?start+prefix.length+4:start+prefix.length+token.length+suffix.length;el.focus();el.setSelectionRange(p,p)})}
}
};
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()}}}
+2 -1
View File
@@ -1,6 +1,7 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<a class="btn btn-sm btn-outline-secondary mb-3" href="{{ request.GET.next|default:'/' }}">← zurück</a>
<a class="btn btn-sm btn-outline-secondary mb-3" href="{{ request.GET.next|default:'/' }}">← {% trans "back" %}</a>
{% include 'core/_item.html' %}
{% if item.backlinks.all %}
<div class="mt-4"><h5>Backlinks</h5><div class="vstack gap-3">{% for item in item.backlinks.all %}{% include 'core/_item.html' %}{% endfor %}</div></div>
+33 -32
View File
@@ -1,47 +1,47 @@
{% extends 'core/base.html' %}
{% load querystring %}
{% load querystring i18n %}
{% 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-primary flex-fill" href="{% url 'new_item' %}?kind=todo&next={{ request.get_full_path|urlencode }}">{% trans "Task" %}</a>
<a class="btn btn-sm btn-secondary flex-fill" href="{% url 'new_item' %}?kind=note&next={{ request.get_full_path|urlencode }}">{% trans "Note" %}</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">
<div class="fw-semibold mb-2">Filter</div>
<div class="fw-semibold mb-2">{% trans "Filter" %}</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<a class="btn btn-sm {% if active_kind == 'todo' %}btn-primary{% else %}btn-outline-primary{% endif %}" href="{% if active_kind == 'todo' %}{% query_replace kind=None %}{% else %}{% query_replace kind='todo' %}{% endif %}">Todos{% if active_kind == 'todo' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'note' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" href="{% if active_kind == 'note' %}{% query_replace kind=None %}{% else %}{% query_replace kind='note' %}{% endif %}">Notizen{% if active_kind == 'note' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'note' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" href="{% if active_kind == 'note' %}{% query_replace kind=None %}{% else %}{% query_replace kind='note' %}{% endif %}">{% trans "Notes" %}{% if active_kind == 'note' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'link' %}btn-success{% else %}btn-outline-success{% endif %}" href="{% if active_kind == 'link' %}{% query_replace kind=None %}{% else %}{% query_replace kind='link' %}{% endif %}">Links{% if active_kind == 'link' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'open' %}btn-warning{% else %}btn-outline-warning{% endif %}" href="{% if active_status == 'open' %}{% query_replace status=None %}{% else %}{% query_replace status='open' %}{% endif %}">Unerledigt{% if active_status == 'open' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'archive' %}btn-dark{% else %}btn-outline-dark{% endif %}" href="{% if active_status == 'archive' %}{% query_replace status=None %}{% else %}{% query_replace status='archive' %}{% endif %}">Archiv{% if active_status == 'archive' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'open' %}btn-warning{% else %}btn-outline-warning{% endif %}" href="{% if active_status == 'open' %}{% query_replace status=None %}{% else %}{% query_replace status='open' %}{% endif %}">{% trans "Open" %}{% if active_status == 'open' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'archive' %}btn-dark{% else %}btn-outline-dark{% endif %}" href="{% if active_status == 'archive' %}{% query_replace status=None %}{% else %}{% query_replace status='archive' %}{% endif %}">{% trans "Archive" %}{% if active_status == 'archive' %} ×{% endif %}</a>
</div>
<form class="mb-3 position-relative" method="get" x-data="searchBox([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:keydown.window="if(($event.ctrlKey||$event.metaKey)&&$event.key==='f'){ $event.preventDefault(); $refs.search.focus(); $refs.search.select(); }">
{% if active_tag %}<input type="hidden" name="tag" value="{{ active_tag }}">{% endif %}
{% if active_kind %}<input type="hidden" name="kind" value="{{ active_kind }}">{% endif %}
{% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %}
<div class="input-group input-group-sm">
<input x-ref="search" class="form-control" name="q" value="{{ q }}" placeholder="Suche (Ctrl+F, # für Tags)" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" @input="onInput($event)" @keydown="onKeydown($event)">
{% if q %}<a class="btn btn-outline-secondary" href="{% query_replace q=None %}" title="Suche löschen">×</a>{% endif %}
<input x-ref="search" class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search (Ctrl+F, # for tags)' %}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" @input="onInput($event)" @keydown="onKeydown($event)">
{% if q %}<a class="btn btn-outline-secondary" href="{% query_replace q=None %}" title="{% trans 'Clear search' %}">×</a>{% endif %}
</div>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(tag, idx) in filtered" :key="tag"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="select(tag)"><strong x-text="'#'+tag"></strong></button></template>
</div>
</form>
<div class="d-none d-md-block">
<div class="fw-semibold mb-2">Tags</div>
<div class="fw-semibold mb-2">{% trans "Tags" %}</div>
<div hx-get="{% url 'tag_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-trigger="tagsChanged from:body" hx-target="#tag-list" hx-swap="outerHTML">
{% include 'core/_tags.html' %}
</div>
</div>
<details class="d-md-none">
<summary class="btn btn-sm btn-outline-secondary mb-2">Tags anzeigen</summary>
<summary class="btn btn-sm btn-outline-secondary mb-2">{% trans "Show tags" %}</summary>
<div class="d-flex flex-wrap gap-2">
{% for tag in tags %}
{% if active_tag == tag.name %}<a class="badge rounded-pill text-bg-dark text-decoration-none tag" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>{% else %}<a class="badge rounded-pill text-bg-light text-decoration-none tag" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>{% endif %}
{% empty %}<span class="text-muted small">Noch keine Tags</span>{% endfor %}
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div>
</details>
</div></div>
@@ -50,26 +50,26 @@
<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-primary flex-fill" @click="insert('/todo')">{% trans "Task" %}</button>
<button type="button" class="btn btn-sm btn-secondary flex-fill" @click="$el.querySelector('textarea').focus()">{% trans "Note" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">{% trans "Add tags" %}</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="addToken('#{{ 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 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 class="mobile-media gap-2 w-100"><label class="btn btn-outline-secondary flex-fill mb-0">{% trans "Gallery" %}<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">{% trans "Photo" %}<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' %}">{% trans "Editor" %}</a>
<button class="btn btn-dark px-4">{% trans "Save" %}</button>
</div>
<template x-if="preview"><div class="mt-2 small"><span class="text-muted">Bild aus Zwischenablage:</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:160px"></div></template>
<template x-if="preview"><div class="mt-2 small"><span class="text-muted">{% trans "Image from clipboard:" %}</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:160px"></div></template>
</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">Noch nichts vorhanden.</div>{% endfor %}
{% for item in items %}{% include 'core/_item.html' %}{% empty %}<div class="text-center text-muted py-5">{% trans "Nothing here yet." %}</div>{% endfor %}
</div>
</section>
</div>
@@ -96,26 +96,27 @@ function quickComposer(tags, itemId=null){
});
},
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'}
{token:'/todo',label:'{% trans "Create task" %}'},
{token:'/link',label:'{% trans "Save link" %}'},
{token:'/journal',label:'{% trans "Journal entry" %}'},
{token:'/public',label:'{% trans "public" %}'},
{token:'/private',label:'{% trans "private" %}'},
{token:'/code',label:'{% trans "Insert Markdown code block" %}'},
{token:'[[',label:'{% trans "Search internal task/note" %}'},
{token:'![[',label:'{% trans "Insert image from attachments" %}'}
],
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}`})))
? (this.itemId?this.attachmentSuggestions.map(a=>({token:`![[file:${a.id}]]`,label:`{% trans "Image:" %} ${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'}];
},
isContentField(el){return el && (el.name==='content' || el.id==='id_content')},
onInput(e){this.update(e.target)},
handlePaste(e){
let item=[...(e.clipboardData?.items||[])].find(i=>i.kind==='file'&&i.type.startsWith('image/'));
@@ -170,7 +171,7 @@ function quickComposer(tags, itemId=null){
},
update(el){
this.currentEl=el;
this.text=el.value;
if(this.isContentField(el)) this.text=el.value;
let before=el.value.slice(0,el.selectionStart);
let fragment=(before.match(/(^|\s)([^\s]*)$/)||['','',''])[2];
this.query=fragment;
@@ -191,7 +192,7 @@ function quickComposer(tags, itemId=null){
let after=el.value.slice(pos);
let prefix=before&&!before.endsWith(' ')&&!before.endsWith('\n')?' ':'';
let value=before+prefix+token+' '+after;
this.text=value;
if(this.isContentField(el)) this.text=value;
el.value=value;
this.showTags=false;
this.$nextTick(()=>{let p=before.length+prefix.length+token.length+1;el.focus();el.setSelectionRange(p,p)})
@@ -210,7 +211,7 @@ function quickComposer(tags, itemId=null){
let prefix=isCode?(start>0&&el.value[start-1]!=='\n'?'\n':''):(needsPrefix?' ':'');
let suffix=isCode?'':' ';
let value=el.value.slice(0,start)+prefix+token+suffix+after;
this.text=value;
if(this.isContentField(el)) this.text=value;
el.value=value;
this.open=false;
this.$nextTick(()=>{
+10 -9
View File
@@ -1,33 +1,34 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<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 %}], {{ item.id }})" x-init="text=$el.querySelector('textarea[name=content]').value">
{% csrf_token %}
<div class="card-body vstack gap-3">
<div class="d-flex justify-content-between align-items-center gap-2 sticky-top bg-white py-2" style="z-index:5">
<h1 class="h5 m-0">Eintrag #{{ item.id }} bearbeiten</h1>
<div class="d-flex gap-2"><button class="btn btn-success px-3 py-2" title="Speichern">✓</button><a class="btn btn-danger px-3 py-2" href="{{ next_url }}" title="Abbrechen">×</a></div>
<h1 class="h5 m-0">{% blocktrans with id=item.id %}Edit item #{{ id }}{% endblocktrans %}</h1>
<div class="d-flex gap-2"><button class="btn btn-success px-3 py-2" title="{% trans 'Save' %}">✓</button><a class="btn btn-danger px-3 py-2" href="{{ next_url }}" title="{% trans 'Cancel' %}">×</a></div>
</div>
<div class="small text-muted">Typ: {{ item.get_kind_display }}</div>
<div class="small text-muted">{% trans "Type:" %} {{ 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>
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">{% trans "Add tags" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></div>
<div class="card-body"><div class="d-flex justify-content-between align-items-center mb-2"><strong>{% trans "Choose tag" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ 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>
<textarea class="form-control" name="comment" rows="5" placeholder="{% trans 'Comment' %}" @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>
<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="row g-2">
<div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>Privat</option><option value="public" {% if item.visibility == 'public' %}selected{% endif %}>Öffentlich</option></select></div>
<div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option><option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option></select></div>
{% if item.kind == 'todo' %}<div class="col-md-4"><input class="form-control" type="datetime-local" name="due_at" value="{{ item.due_at|date:'Y-m-d\\TH:i' }}"></div>{% endif %}
<div class="col-md-4"><input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)"></div>
</div>
{% if item.kind == 'link' %}<input class="form-control" type="url" name="url" value="{{ item.url }}" placeholder="URL">{% endif %}
<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>
<div class="d-flex gap-2 d-none d-md-flex"><button class="btn btn-dark">Speichern</button><a class="btn btn-outline-secondary" href="{{ next_url }}">Abbrechen</a></div>
<template x-if="preview"><div class="small"><span class="text-muted">{% trans "Image preview:" %}</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:180px"></div></template>
<div class="d-flex gap-2 d-none d-md-flex"><button class="btn btn-dark">{% trans "Save" %}</button><a class="btn btn-outline-secondary" href="{{ next_url }}">{% trans "Cancel" %}</a></div>
</div>
</form>
{% endblock %}
+7 -7
View File
@@ -1,5 +1,5 @@
{% extends 'core/base.html' %}
{% load querystring %}
{% load querystring i18n %}
{% block content %}
<div class="row g-4">
<aside class="col-lg-3 order-lg-2">
@@ -12,7 +12,7 @@
<table class="table table-sm text-center mb-3"><thead><tr><th>Mo</th><th>Di</th><th>Mi</th><th>Do</th><th>Fr</th><th>Sa</th><th>So</th></tr></thead><tbody>
{% for week in weeks %}<tr>{% for d in week %}<td>{% if d.day %}{% if d.has_entries %}<a class="btn btn-sm {% if selected_day == d.date %}btn-info{% else %}btn-outline-info{% endif %}" href="?day={{ d.date }}&year={{ year }}&month={{ month }}">{{ d.day }}</a>{% else %}<span class="small text-muted">{{ d.day }}</span>{% endif %}{% endif %}</td>{% endfor %}</tr>{% endfor %}
</tbody></table>
<div class="fw-semibold mb-2">Tags</div>
<div class="fw-semibold mb-2">{% trans "Tags" %}</div>
{% include 'core/_tags.html' %}
</div></div>
</aside>
@@ -22,17 +22,17 @@
{% 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-primary flex-fill" @click="insert('/todo')">{% trans "Task" %}</button>
<button type="button" class="btn btn-sm btn-secondary flex-fill" @click="$el.querySelector('textarea').focus()">{% trans "Note" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">{% trans "Add tags" %}</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="addToken('#{{ 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 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>
<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">{% trans "Gallery" %}<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">{% trans "Photo" %}<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div><button class="btn btn-dark px-4">{% trans "Save" %}</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 %}
{% for item in items %}{% include 'core/_item.html' %}{% empty %}<div class="text-center text-muted py-5">{% trans "No journal activity on this day." %}</div>{% endfor %}
</div>
</section>
</div>
+3 -2
View File
@@ -1,12 +1,13 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'kanban_list' %}">← Kanbans</a>
<h1 class="h4 mb-3">{{ kanban.name }} <span class="badge text-bg-light">#{{ kanban.tag.name }}</span></h1>
<div class="row g-3" x-data="kanbanBoard({{ kanban.id }})">
<section class="col-lg-3 col-md-6"><div class="card item-card shadow-sm h-100"><div class="card-body"><h2 class="h6">Eingang</h2><div class="kanban-column vstack gap-2 p-2 rounded bg-light" data-status="inbox" @dragover.prevent @drop="drop($event)">{% for item in inbox_items %}{% include 'core/_kanban_card.html' %}{% empty %}<div class="text-muted small">Leer.</div>{% endfor %}</div></div></div></section>
<section class="col-lg-3 col-md-6"><div class="card item-card shadow-sm h-100"><div class="card-body"><h2 class="h6">{% trans "Inbox" %}</h2><div class="kanban-column vstack gap-2 p-2 rounded bg-light" data-status="inbox" @dragover.prevent @drop="drop($event)">{% for item in inbox_items %}{% include 'core/_kanban_card.html' %}{% empty %}<div class="text-muted small">Leer.</div>{% endfor %}</div></div></div></section>
<section class="col-lg-3 col-md-6"><div class="card item-card shadow-sm h-100"><div class="card-body"><h2 class="h6">Todo</h2><div class="kanban-column vstack gap-2 p-2 rounded bg-light" data-status="todo" @dragover.prevent @drop="drop($event)">{% for item in todo_items %}{% include 'core/_kanban_card.html' %}{% empty %}<div class="text-muted small">Leer.</div>{% endfor %}</div></div></div></section>
<section class="col-lg-3 col-md-6"><div class="card item-card shadow-sm h-100"><div class="card-body"><h2 class="h6">In Progress</h2><div class="kanban-column vstack gap-2 p-2 rounded bg-light" data-status="progress" @dragover.prevent @drop="drop($event)">{% for item in progress_items %}{% include 'core/_kanban_card.html' %}{% empty %}<div class="text-muted small">Leer.</div>{% endfor %}</div></div></div></section>
<section class="col-lg-3 col-md-6"><div class="card item-card shadow-sm h-100"><div class="card-body"><h2 class="h6">Erledigt</h2><div class="kanban-column vstack gap-2 p-2 rounded bg-light" data-status="done" @dragover.prevent @drop="drop($event)">{% for item in done_items %}{% include 'core/_kanban_card.html' %}{% empty %}<div class="text-muted small">Leer.</div>{% endfor %}</div></div></div></section>
<section class="col-lg-3 col-md-6"><div class="card item-card shadow-sm h-100"><div class="card-body"><h2 class="h6">{% trans "Done" %}</h2><div class="kanban-column vstack gap-2 p-2 rounded bg-light" data-status="done" @dragover.prevent @drop="drop($event)">{% for item in done_items %}{% include 'core/_kanban_card.html' %}{% empty %}<div class="text-muted small">Leer.</div>{% endfor %}</div></div></div></section>
</div>
<script>
function kanbanBoard(id){return{item:null,drag(e){this.item=e.target.closest('[data-id]')},drop(e){let col=e.target.closest('[data-status]');if(!this.item||!col)return;col.querySelectorAll('.text-muted.small').forEach(e=>e.remove());col.appendChild(this.item);this.item.classList.toggle('done', col.dataset.status==='done');fetch(`/kanban/${id}/move/${this.item.dataset.id}/`,{method:'POST',headers:{'X-CSRFToken':'{{ csrf_token }}','Content-Type':'application/x-www-form-urlencoded'},body:`status=${col.dataset.status}`});}}}
+4 -3
View File
@@ -1,8 +1,9 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3"><h1 class="h4 m-0">Kanbans</h1><a class="btn btn-sm btn-outline-secondary" href="/">← zurück</a></div>
<div class="d-flex justify-content-between align-items-center mb-3"><h1 class="h4 m-0">Kanbans</h1><a class="btn btn-sm btn-outline-secondary" href="/">← {% trans "back" %}</a></div>
<div class="row g-4">
<section class="col-lg-5"><div class="card item-card shadow-sm"><div class="card-body"><h2 class="h5">Neues Kanban</h2><form method="post" class="vstack gap-2">{% csrf_token %}{{ form.name }}{{ form.tag_name }}<button class="btn btn-dark">Anlegen</button></form></div></div></section>
<section class="col-lg-7"><div class="vstack gap-3">{% for s in stats %}<a class="card item-card shadow-sm text-decoration-none text-dark" href="{% url 'kanban_detail' s.kanban.id %}"><div class="card-body"><div class="d-flex justify-content-between"><strong>{{ s.kanban.name }}</strong><span class="badge text-bg-light">#{{ s.kanban.tag.name }}</span></div><div class="small text-muted mt-2">Offen: {{ s.open }} · Erledigt: {{ s.done }}</div></div></a>{% empty %}<div class="text-muted">Noch keine Kanbans.</div>{% endfor %}</div></section>
<section class="col-lg-5"><div class="card item-card shadow-sm"><div class="card-body"><h2 class="h5">{% trans "New Kanban" %}</h2><form method="post" class="vstack gap-2">{% csrf_token %}{{ form.name }}{{ form.tag_name }}<button class="btn btn-dark">{% trans "Create" %}</button></form></div></div></section>
<section class="col-lg-7"><div class="vstack gap-3">{% for s in stats %}<a class="card item-card shadow-sm text-decoration-none text-dark" href="{% url 'kanban_detail' s.kanban.id %}"><div class="card-body"><div class="d-flex justify-content-between"><strong>{{ s.kanban.name }}</strong><span class="badge text-bg-light">#{{ s.kanban.tag.name }}</span></div><div class="small text-muted mt-2">{% trans "Open" %}: {{ s.open }} · {% trans "Done" %}: {{ s.done }}</div></div></a>{% empty %}<div class="text-muted">{% trans "No Kanbans yet." %}</div>{% endfor %}</div></section>
</div>
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends 'core/base.html' %}
{% block content %}
<section class="py-4 py-md-5">
<div class="row align-items-center g-4">
<div class="col-lg-7">
<span class="badge text-bg-dark mb-3">Deutsch</span>
<h1 class="display-5 fw-bold mb-3">my2dos – Aufgaben, Notizen, Links und Teams an einem Ort</h1>
<p class="lead text-muted">my2dos ist eine schnelle persönliche Wissens- und Aufgaben-App. Du schreibst einfach los, nutzt Tags zur Strukturierung und kannst Einträge privat behalten oder mit deinem Team teilen.</p>
<div class="d-flex flex-wrap gap-2 mt-4">
<a class="btn btn-dark btn-lg" href="{% url 'login' %}">Einloggen</a>
<a class="btn btn-outline-secondary btn-lg" href="#tutorial">Tutorial ansehen</a>
</div>
</div>
<div class="col-lg-5">
<div class="card item-card shadow-sm"><div class="card-body">
<div class="small text-muted mb-2">Beispiel</div>
<pre class="mb-0 bg-dark text-white rounded p-3"><code>/todo Deployment vorbereiten #my2dos
/link https://example.com #research
/private Persönliche Idee #my2dos
[[42]] verlinkt einen anderen Eintrag</code></pre>
</div></div>
</div>
</div>
</section>
<section id="tutorial" class="mb-5">
<div class="row g-4">
<div class="col-md-6 col-lg-3"><div class="card item-card h-100 shadow-sm"><div class="card-body"><h2 class="h5">1. Schnell erfassen</h2><p class="text-muted mb-0">Schreibe Text direkt in das Eingabefeld. Ohne Befehl wird daraus eine Notiz.</p></div></div></div>
<div class="col-md-6 col-lg-3"><div class="card item-card h-100 shadow-sm"><div class="card-body"><h2 class="h5">2. Befehle nutzen</h2><p class="text-muted mb-0"><code>/todo</code>, <code>/note</code>, <code>/link</code>, <code>/journal</code>, <code>/public</code> oder <code>/private</code>.</p></div></div></div>
<div class="col-md-6 col-lg-3"><div class="card item-card h-100 shadow-sm"><div class="card-body"><h2 class="h5">3. Mit Tags ordnen</h2><p class="text-muted mb-0">Tags wie <code>#arbeit</code> oder <code>#idee</code> filtern Einträge und können Kanbans bilden.</p></div></div></div>
<div class="col-md-6 col-lg-3"><div class="card item-card h-100 shadow-sm"><div class="card-body"><h2 class="h5">4. Teams teilen</h2><p class="text-muted mb-0">Ein Team-Tag wie <code>#my2dos</code> macht einen Eintrag automatisch für das Team sichtbar.</p></div></div></div>
</div>
</section>
<hr class="my-5">
<section class="py-4">
<div class="row align-items-center g-4">
<div class="col-lg-7">
<span class="badge text-bg-secondary mb-3">English</span>
<h1 class="display-6 fw-bold mb-3">my2dos – tasks, notes, links and teams in one place</h1>
<p class="lead text-muted">my2dos is a fast personal productivity and knowledge app. Capture thoughts quickly, organize them with tags, keep things private or share them with your team.</p>
<a class="btn btn-dark btn-lg" href="{% url 'login' %}">Log in</a>
</div>
<div class="col-lg-5">
<div class="card item-card shadow-sm"><div class="card-body">
<h2 class="h5">Quick tutorial</h2>
<ol class="text-muted mb-0">
<li>Write anything to create a note.</li>
<li>Use <code>/todo</code>, <code>/link</code> or <code>/journal</code> to choose the type.</li>
<li>Add tags like <code>#work</code> to organize and filter.</li>
<li>Use a team tag like <code>#my2dos</code> to create shared team items.</li>
<li>Use <code>/private</code> to force a private item.</li>
</ol>
</div></div>
</div>
</div>
</section>
{% endblock %}
+5 -4
View File
@@ -1,13 +1,14 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<div class="row justify-content-center"><div class="col-md-5">
<div class="card item-card shadow-sm"><div class="card-body">
<h1 class="h4 mb-3">Anmelden</h1>
{% if form.errors %}<div class="alert alert-danger">Benutzername oder Passwort ist falsch.</div>{% endif %}
<h1 class="h4 mb-3">{% trans "Login" %}</h1>
{% if form.errors %}<div class="alert alert-danger">{% trans "Username or password is incorrect." %}</div>{% endif %}
<form method="post">
{% csrf_token %}
<div class="mb-3"><label class="form-label">Benutzername</label><input class="form-control" name="username" autofocus></div>
<div class="mb-3"><label class="form-label">Passwort</label><input class="form-control" type="password" name="password"></div>
<div class="mb-3"><label class="form-label">{% trans "Username" %}</label><input class="form-control" name="username" autofocus></div>
<div class="mb-3"><label class="form-label">{% trans "Password" %}</label><input class="form-control" type="password" name="password"></div>
<button class="btn btn-dark w-100">Login</button>
</form>
</div></div>
+10 -9
View File
@@ -1,18 +1,19 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<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">
<div class="d-flex justify-content-between align-items-center gap-2 sticky-top bg-white py-2" style="z-index:5">
<h1 class="h5 m-0">{% 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="d-flex gap-2"><button class="btn btn-success px-3 py-2" title="Speichern">✓</button><a class="btn btn-danger px-3 py-2" href="{{ next_url }}" title="Abbrechen">×</a></div>
<h1 class="h5 m-0">{% if requested_kind == 'todo' %}{% trans "Create task" %}{% elif requested_kind == 'link' %}{% trans "Create link" %}{% elif requested_kind == 'journal' %}{% trans "Create journal entry" %}{% else %}{% trans "Create note" %}{% endif %}</h1>
<div class="d-flex gap-2"><button class="btn btn-success px-3 py-2" title="{% trans 'Save' %}">✓</button><a class="btn btn-danger px-3 py-2" href="{{ next_url }}" title="{% trans 'Cancel' %}">×</a></div>
</div>
<div class="mobile-tag-picker">
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">Tags hinzufügen</button>
<button type="button" class="btn btn-sm btn-outline-secondary" @click="showTags=true">{% trans "Add tags" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></div>
<div class="card-body"><div class="d-flex justify-content-between align-items-center mb-2"><strong>{% trans "Choose tag" %}</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="addToken('#{{ tag.name|escapejs }}')">#{{ 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>
@@ -20,13 +21,13 @@
<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">Bild hinzufügen</label>
<label class="form-label">{% trans "Add image" %}</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 class="mobile-media gap-2"><label class="btn btn-outline-secondary flex-fill mb-0">{% trans "Gallery" %}<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">{% trans "Photo" %}<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div>
<div class="form-text">{% trans "Use" %} <code>![[</code> {% trans "to insert images into the text." %}</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 d-none d-md-inline-block">Speichern</button>
<template x-if="preview"><div class="small"><span class="text-muted">{% trans "Image preview:" %}</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:180px"></div></template>
<button class="btn btn-dark align-self-start d-none d-md-inline-block">{% trans "Save" %}</button>
</div>
</form>
{% endblock %}
+29 -16
View File
@@ -1,22 +1,23 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% 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="/">← {% trans "back" %}</a>
<div class="row g-4">
<section class="col-lg-6">
<div class="card item-card shadow-sm"><div class="card-body">
<h1 class="h4 mb-3">Benutzereinstellungen</h1>
<h1 class="h4 mb-3">{% trans "User settings" %}</h1>
<form method="post" class="vstack gap-3">
{% csrf_token %}<input type="hidden" name="action" value="profile">
<div><label class="form-label">Vorname</label>{{ profile_form.first_name }}</div>
<div><label class="form-label">Name</label>{{ profile_form.last_name }}</div>
<div><label class="form-label">E-Mail</label>{{ profile_form.email }}</div>
<button class="btn btn-dark">Speichern</button>
<div><label class="form-label">{% trans "First name" %}</label>{{ profile_form.first_name }}</div>
<div><label class="form-label">{% trans "Last name" %}</label>{{ profile_form.last_name }}</div>
<div><label class="form-label">{% trans "Email" %}</label>{{ profile_form.email }}</div>
<button class="btn btn-dark">{% trans "Save" %}</button>
</form>
</div></div>
</section>
<section class="col-lg-6">
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 class="h5 mb-3">Standardansicht</h2>
<h2 class="h5 mb-3">{% trans "Default view" %}</h2>
<form method="post" class="vstack gap-2">
{% csrf_token %}<input type="hidden" name="action" value="preferences">
{% for field in preference_form %}
@@ -26,28 +27,40 @@
<div><label class="form-label">{{ field.label }}</label>{{ field }}</div>
{% endif %}
{% endfor %}
<button class="btn btn-dark mt-2">Standardansicht speichern</button>
<button class="btn btn-dark mt-2">{% trans "Save default view" %}</button>
</form>
</div></div>
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 class="h5 mb-3">API Key erzeugen</h2>
<h2 id="teams" class="h5 mb-3">{% trans "Teams" %}</h2>
<form method="post" class="vstack gap-3 mb-3">
{% csrf_token %}<input type="hidden" name="action" value="team">
<div><label class="form-label">{% trans "Team name" %}</label>{{ team_form.name }}</div>
<div><label class="form-label">{% trans "Team tag" %}</label><div class="small text-muted mb-2">{% trans "This tag automatically turns items into team items, e.g. #my2dos." %}</div>{{ team_form.slug }}</div>
<button class="btn btn-dark">{% trans "Create team" %}</button>
</form>
<div class="vstack gap-2">
{% for team in teams %}<a class="border rounded p-2 text-decoration-none" href="{% url 'team_detail' team.id %}"><strong>{{ team.name }}</strong> <span class="text-muted">#{{ team.slug }}</span></a>{% empty %}<span class="text-muted">{% trans "No teams yet." %}</span>{% endfor %}
</div>
</div></div>
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 class="h5 mb-3">{% trans "Create API key" %}</h2>
<form method="post" class="vstack gap-3">
{% csrf_token %}<input type="hidden" name="action" value="apikey">
<div><label class="form-label">Name</label>{{ key_form.name }}</div>
<div><label class="form-label">Auf Tags beschränken</label><div class="small text-muted mb-2">Keine Auswahl = Zugriff auf alle eigenen Einträge.</div>{{ key_form.tags }}</div>
<button class="btn btn-dark">API Key generieren</button>
<div><label class="form-label">{% trans "Name" %}</label>{{ key_form.name }}</div>
<div><label class="form-label">{% trans "Restrict to tags" %}</label><div class="small text-muted mb-2">{% trans "No selection = access to all own items." %}</div>{{ key_form.tags }}</div>
<button class="btn btn-dark">{% trans "Generate API key" %}</button>
</form>
</div></div>
<div class="card item-card shadow-sm"><div class="card-body">
<h2 class="h5 mb-3">API Keys</h2>
<h2 class="h5 mb-3">{% trans "API keys" %}</h2>
<div class="vstack gap-2">
{% for key in api_keys %}
<div class="border rounded p-2">
<div class="d-flex justify-content-between gap-2"><strong>{{ key.name }}</strong><form method="post" action="{% url 'delete_api_key' key.id %}">{% csrf_token %}<button class="btn btn-sm btn-outline-danger">Löschen</button></form></div>
<div class="d-flex justify-content-between gap-2"><strong>{{ key.name }}</strong><form method="post" action="{% url 'delete_api_key' key.id %}">{% csrf_token %}<button class="btn btn-sm btn-outline-danger">{% trans "Delete" %}</button></form></div>
<code class="small user-select-all">{{ key.token }}</code>
<div class="small text-muted">Tags: {% for tag in key.tags.all %}#{{ tag.name }} {% empty %}alle{% endfor %}</div>
<div class="small text-muted">{% trans "Tags" %}: {% for tag in key.tags.all %}#{{ tag.name }} {% empty %}{% trans "all" %}{% endfor %}</div>
</div>
{% empty %}<span class="text-muted">Noch keine API Keys.</span>{% endfor %}
{% empty %}<span class="text-muted">{% trans "No API keys yet." %}</span>{% endfor %}
</div>
</div></div>
</section>
+40
View File
@@ -0,0 +1,40 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'settings' %}">← {% trans "Settings" %}</a>
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h1 class="h3 mb-1">{% trans "Team" %} {{ team.name }}</h1>
<div class="text-muted">{% trans "Team tag:" %} <a href="/?tag={{ team.slug }}">#{{ team.slug }}</a></div>
</div>
<a class="btn btn-dark" href="/?tag={{ team.slug }}">{% blocktrans with slug=team.slug %}Create item with #{{ slug }}{% endblocktrans %}</a>
</div>
<div class="row g-4">
<section class="col-lg-4">
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 class="h5">{% trans "Members" %}</h2>
<div class="vstack gap-2">
{% for m in memberships %}<div class="border rounded p-2"><strong>{{ m.user.get_full_name|default:m.user.username }}</strong><div class="small text-muted">{{ m.get_role_display }}</div></div>{% endfor %}
</div>
</div></div>
{% if membership.role == 'owner' or membership.role == 'admin' %}
<div class="card item-card shadow-sm"><div class="card-body">
<h2 class="h5">{% trans "Invite" %}</h2>
<form method="post" class="vstack gap-2">
{% csrf_token %}
<div>{{ invite_form.email }}</div>
<button class="btn btn-dark">{% trans "Create invitation link" %}</button>
</form>
<div class="small text-muted mt-3">{% trans "Open invitations:" %}</div>
{% for invite in invites %}<div class="small border rounded p-2 mt-2">{{ invite.email }}<br><code>{{ request.scheme }}://{{ request.get_host }}{% url 'team_accept_invite' invite.token %}</code></div>{% empty %}<div class="small text-muted">{% trans "No open invitations." %}</div>{% endfor %}
</div></div>
{% endif %}
</section>
<section class="col-lg-8">
<h2 class="h5 mb-3">{% trans "Team items" %}</h2>
<div class="vstack gap-3">
{% for item in items %}{% include 'core/_item.html' %}{% empty %}<div class="text-muted">{% blocktrans with slug=team.slug %}No team items yet. Create something with #{{ slug }}.{% endblocktrans %}</div>{% endfor %}
</div>
</section>
</div>
{% endblock %}