Improve teams settings invites and due tasks
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
<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="{% trans 'Date/time for todo' %}"></div>{% endif %}
|
||||
{% if item.kind == 'todo' %}<div class="col-md-3"><input class="form-control" type="date" name="due_date" value="{{ item.due_at|date:'Y-m-d' }}" title="{% trans 'Due date' %}"></div><div class="col-md-3"><input class="form-control" name="due_time" list="due-times-{{ item.id }}" value="{{ item.due_at|date:'H:i' }}" placeholder="{% trans 'Time optional' %}" title="{% trans 'Time optional' %}"><datalist id="due-times-{{ item.id }}"><option value="00:00"><option value="00:30"><option value="01:00"><option value="01:30"><option value="02:00"><option value="02:30"><option value="03:00"><option value="03:30"><option value="04:00"><option value="04:30"><option value="05:00"><option value="05:30"><option value="06:00"><option value="06:30"><option value="07:00"><option value="07:30"><option value="08:00"><option value="08:30"><option value="09:00"><option value="09:30"><option value="10:00"><option value="10:30"><option value="11:00"><option value="11:30"><option value="12:00"><option value="12:30"><option value="13:00"><option value="13:30"><option value="14:00"><option value="14:30"><option value="15:00"><option value="15:30"><option value="16:00"><option value="16:30"><option value="17:00"><option value="17:30"><option value="18:00"><option value="18:30"><option value="19:00"><option value="19:30"><option value="20:00"><option value="20:30"><option value="21:00"><option value="21:30"><option value="22:00"><option value="22:30"><option value="23:00"><option value="23:30"></datalist></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 %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% 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 }}'">
|
||||
<article id="item-{{ item.id }}" class="card item-card shadow-sm {% if item.is_done %}done{% elif item.is_overdue %}border-danger border-2{% elif item.is_due_today %}border-warning border-2{% 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">
|
||||
@@ -14,7 +14,7 @@
|
||||
{% 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">{% trans "Due:" %} {{ item.due_at|date:'d.m.Y H:i' }}</div>{% endif %}
|
||||
{% if item.due_at %}<div class="small mt-2 {% if item.is_overdue %}text-danger fw-semibold{% elif item.is_due_today %}text-warning-emphasis fw-semibold{% else %}text-warning-emphasis{% endif %}">{% if item.is_overdue %}<span class="badge text-bg-danger me-1">{% trans "Overdue" %}</span>{% elif item.is_due_today %}<span class="badge text-bg-warning me-1">{% trans "Due today" %}</span>{% endif %}{% 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">{% 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 %}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% 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 %}
|
||||
<a class="badge rounded-pill text-bg-dark text-decoration-none tag" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>
|
||||
{% if tag.name in active_tags %}
|
||||
<a class="badge rounded-pill text-bg-dark text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" 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>
|
||||
<a class="badge rounded-pill text-bg-light text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>
|
||||
{% endif %}
|
||||
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center"><div class="col-md-6">
|
||||
<div class="card item-card shadow-sm"><div class="card-body">
|
||||
<h1 class="h4 mb-2">{% trans "Create account" %}</h1>
|
||||
<p class="text-muted">{% trans "You were invited to my2dos. Create your account to continue." %}</p>
|
||||
<form method="post" class="vstack gap-3">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
|
||||
{% for field in form %}
|
||||
<div>
|
||||
<label class="form-label">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
|
||||
{% for error in field.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-dark">{% trans "Create account" %}</button>
|
||||
</form>
|
||||
</div></div>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
@@ -17,11 +17,11 @@
|
||||
<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}.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}}
|
||||
body{background:linear-gradient(180deg,#f8fafc 0,#f7f7f4 18rem);color:#1f2937}.shell{max-width:960px}.navbar{box-shadow:0 .35rem 1.25rem rgba(15,23,42,.06)}.navbar a{text-decoration:none}.nav-soft{color:#475569!important;font-weight:500;padding:.2rem .35rem;border-radius:.5rem}.nav-soft:hover{color:#0d6efd!important;background:#eff6ff}.user-menu-btn{min-width:9rem}.card{border:0}.item-card{border:0;border-radius:1.1rem;box-shadow:0 .45rem 1.4rem rgba(15,23,42,.07)!important}.filter-panel{padding:.25rem .15rem}.border{border-color:rgba(15,23,42,.10)!important}.text-bg-light{background:#f1f5f9!important;color:#475569!important}.btn{border-radius:.7rem}.form-control,.form-select{border-color:#e2e8f0;border-radius:.75rem}.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="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>
|
||||
<nav class="navbar bg-white sticky-top"><div class="container shell"><a class="navbar-brand fw-bold" href="/">my2dos</a><div class="d-flex align-items-center gap-3"><span class="small d-none d-sm-inline"><a class="nav-soft" href="/">{% for label in nav_overview_labels %}{{ label }}{% if not forloop.last %} · {% endif %}{% endfor %}</a>{% for link in nav_hidden_links %}<a class="nav-soft ms-2" href="{{ link.url }}">{{ link.label }}</a>{% endfor %}</span>{% if user.is_authenticated %}<a class="small nav-soft" href="{% url 'journal' %}">{% trans "Journal" %}</a><a class="small nav-soft" href="{% url 'kanban_list' %}">{% trans "Kanban" %}</a><a class="small nav-soft" href="{% url 'team_list' %}">{% trans "Teams" %}</a><div class="dropdown"><button class="btn btn-sm btn-outline-secondary dropdown-toggle user-menu-btn" 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 %}</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 }}'});
|
||||
@@ -45,6 +45,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));
|
||||
document.addEventListener('click', event => {let link=event.target.closest('a.tag-filter[data-tag]');if(!link||!(event.ctrlKey||event.metaKey))return;event.preventDefault();let url=new URL(window.location.href);let tag=link.dataset.tag;let tags=url.searchParams.getAll('tag');url.searchParams.delete('tag');if(tags.includes(tag)){tags=tags.filter(t=>t!==tag)}else{tags.push(tag)}tags.forEach(t=>url.searchParams.append('tag',t));window.location.href=url.pathname+(url.searchParams.toString()?'?'+url.searchParams.toString():'')});
|
||||
let deferredPwaPrompt=null;window.addEventListener('beforeinstallprompt',event=>{event.preventDefault();deferredPwaPrompt=event;document.getElementById('pwa-install')?.classList.remove('d-none')});document.addEventListener('click',async event=>{if(event.target?.id==='pwa-install'&&deferredPwaPrompt){deferredPwaPrompt.prompt();await deferredPwaPrompt.userChoice;deferredPwaPrompt=null;event.target.classList.add('d-none')}});
|
||||
if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('/sw.js').catch(()=>{}));}
|
||||
</script>
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
</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="filter-panel">
|
||||
<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 %}">{% 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 %}">{% trans "Open" %}{% if active_status == 'open' %} ×{% endif %}</a>
|
||||
<a class="btn btn-sm {% if active_status == 'due' %}btn-danger{% else %}btn-outline-danger{% endif %}" href="{% if active_status == 'due' %}{% query_replace status=None %}{% else %}{% query_replace status='due' %}{% endif %}">{% trans "Due" %} <span class="badge {% if active_status == 'due' %}text-bg-light{% else %}text-bg-danger{% endif %}">{{ due_count }}</span>{% if active_status == 'due' %} ×{% 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 %}
|
||||
{% for tag_name in active_tags %}<input type="hidden" name="tag" value="{{ tag_name }}">{% endfor %}
|
||||
{% if active_tag_mode == 'all' %}<input type="hidden" name="tag_mode" value="all">{% 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">
|
||||
@@ -31,20 +33,29 @@
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-none d-md-block">
|
||||
<div class="fw-semibold mb-2">{% trans "Tags" %}</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="fw-semibold">{% trans "Tags" %}</div>
|
||||
{% if active_tags|length > 1 %}
|
||||
<div class="btn-group btn-group-sm" title="{% trans 'Tag mode' %}">
|
||||
<a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a>
|
||||
<a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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">{% trans "Show tags" %}</summary>
|
||||
{% if active_tags|length > 1 %}<div class="btn-group btn-group-sm mb-2"><a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a><a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a></div>{% endif %}
|
||||
<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 %}
|
||||
{% if tag.name in active_tags %}<a class="badge rounded-pill text-bg-dark text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>{% else %}<a class="badge rounded-pill text-bg-light text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>{% endif %}
|
||||
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div></div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="col-lg-9">
|
||||
<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()}">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</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 %}>{% 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 %}
|
||||
{% if item.kind == 'todo' %}<div class="col-md-3"><input class="form-control" type="date" name="due_date" value="{{ item.due_at|date:'Y-m-d' }}" title="{% trans 'Due date' %}"></div><div class="col-md-3"><input class="form-control" name="due_time" list="due-times-{{ item.id }}" value="{{ item.due_at|date:'H:i' }}" placeholder="{% trans 'Time optional' %}" title="{% trans 'Time optional' %}"><datalist id="due-times-{{ item.id }}"><option value="00:00"><option value="00:30"><option value="01:00"><option value="01:30"><option value="02:00"><option value="02:30"><option value="03:00"><option value="03:30"><option value="04:00"><option value="04:30"><option value="05:00"><option value="05:30"><option value="06:00"><option value="06:30"><option value="07:00"><option value="07:30"><option value="08:00"><option value="08:30"><option value="09:00"><option value="09:30"><option value="10:00"><option value="10:30"><option value="11:00"><option value="11:30"><option value="12:00"><option value="12:30"><option value="13:00"><option value="13:30"><option value="14:00"><option value="14:30"><option value="15:00"><option value="15:30"><option value="16:00"><option value="16:30"><option value="17:00"><option value="17:30"><option value="18:00"><option value="18:30"><option value="19:00"><option value="19:30"><option value="20:00"><option value="20:30"><option value="21:00"><option value="21:30"><option value="22:00"><option value="22:30"><option value="23:00"><option value="23:30"></datalist></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 %}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
{% block content %}
|
||||
<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="filter-panel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<a class="btn btn-sm btn-outline-dark" href="?year={{ prev_year }}&month={{ prev_month }}">←</a>
|
||||
<strong>{{ month }}/{{ year }}</strong>
|
||||
<a class="btn btn-sm btn-outline-dark" href="?year={{ next_year }}&month={{ next_month }}">→</a>
|
||||
</div>
|
||||
<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>
|
||||
<table class="table table-sm text-center mb-3 small"><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">{% trans "Tags" %}</div>
|
||||
{% include 'core/_tags.html' %}
|
||||
</div></div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="col-lg-9">
|
||||
<h1 class="h4 mb-3">Journal {{ selected_day }}</h1>
|
||||
|
||||
@@ -2,60 +2,87 @@
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<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">{% trans "User settings" %}</h1>
|
||||
<form method="post" class="vstack gap-3">
|
||||
<div class="row g-4 align-items-start">
|
||||
<aside class="col-lg-3">
|
||||
<div class="card item-card shadow-sm sticky-lg-top" style="top:5rem"><div class="card-body">
|
||||
<h1 class="h5 mb-3">{% trans "Settings" %}</h1>
|
||||
<nav class="nav nav-pills flex-column gap-1">
|
||||
<a class="nav-link" href="#profile">{% trans "Profile" %}</a>
|
||||
<a class="nav-link" href="#preferences">{% trans "View & language" %}</a>
|
||||
<a class="nav-link" href="#invites">{% trans "Invitations" %}</a>
|
||||
<a class="nav-link" href="#api">{% trans "API keys" %}</a>
|
||||
</nav>
|
||||
</div></div>
|
||||
</aside>
|
||||
|
||||
<section class="col-lg-9 vstack gap-4">
|
||||
<div id="profile" class="card item-card shadow-sm"><div class="card-body">
|
||||
<h2 class="h4 mb-1">{% trans "Profile" %}</h2>
|
||||
<p class="text-muted small mb-3">{% trans "Manage your personal user data." %}</p>
|
||||
<form method="post" class="row g-3">
|
||||
{% csrf_token %}<input type="hidden" name="action" value="profile">
|
||||
<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>
|
||||
<div class="col-md-6"><label class="form-label">{% trans "First name" %}</label>{{ profile_form.first_name }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{% trans "Last name" %}</label>{{ profile_form.last_name }}</div>
|
||||
<div class="col-12"><label class="form-label">{% trans "Email" %}</label>{{ profile_form.email }}</div>
|
||||
<div class="col-12"><button class="btn btn-dark">{% trans "Save profile" %}</button></div>
|
||||
</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">{% trans "Default view" %}</h2>
|
||||
<form method="post" class="vstack gap-2">
|
||||
|
||||
<div id="preferences" class="card item-card shadow-sm"><div class="card-body">
|
||||
<h2 class="h4 mb-1">{% trans "View & language" %}</h2>
|
||||
<p class="text-muted small mb-3">{% trans "Choose your language and configure the default overview." %}</p>
|
||||
<form method="post" class="vstack gap-3">
|
||||
{% csrf_token %}<input type="hidden" name="action" value="preferences">
|
||||
{% for field in preference_form %}
|
||||
{% if field.field.widget.input_type == 'checkbox' %}
|
||||
<label class="form-check"><input class="form-check-input" type="checkbox" name="{{ field.name }}" {% if field.value %}checked{% endif %}> <span class="form-check-label">{{ field.label }}</span></label>
|
||||
{% else %}
|
||||
<div><label class="form-label">{{ field.label }}</label>{{ field }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<button class="btn btn-dark mt-2">{% trans "Save default view" %}</button>
|
||||
<div class="row g-3">
|
||||
{% for field in preference_form %}
|
||||
{% if field.name == 'language' or field.name == 'overview_lines' %}
|
||||
<div class="col-md-6"><label class="form-label">{{ field.label }}</label>{{ field }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="col-12">
|
||||
<div class="form-label mb-2">{% trans "Sorting" %}</div>
|
||||
<label class="form-check border rounded p-3"><input class="form-check-input" type="checkbox" name="due_first" {% if preference_form.due_first.value %}checked{% endif %}> <span class="form-check-label fw-semibold">{{ preference_form.due_first.label }}</span><div class="small text-muted">{% trans "Overdue and due-today tasks are sorted to the top of the overview." %}</div></label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-label mb-2">{% trans "Show in overview" %}</div>
|
||||
<div class="row g-2">
|
||||
{% for field in preference_form %}
|
||||
{% if field.field.widget.input_type == 'checkbox' and field.name != 'due_first' %}
|
||||
<div class="col-md-6"><label class="form-check border rounded p-2 ps-4 h-100"><input class="form-check-input" type="checkbox" name="{{ field.name }}" {% if field.value %}checked{% endif %}> <span class="form-check-label">{{ field.label }}</span></label></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div><button class="btn btn-dark">{% trans "Save view settings" %}</button></div>
|
||||
</form>
|
||||
</div></div>
|
||||
<div class="card item-card shadow-sm mb-3"><div class="card-body">
|
||||
<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>
|
||||
|
||||
<div id="invites" class="card item-card shadow-sm"><div class="card-body">
|
||||
<h2 class="h4 mb-1">{% trans "User invitations" %}</h2>
|
||||
<p class="text-muted small mb-3">{% trans "Invite new users during the test phase. Only people with an invitation link can register." %}</p>
|
||||
<form method="post" class="row g-3 mb-4">
|
||||
{% csrf_token %}<input type="hidden" name="action" value="userinvite">
|
||||
<div class="col-md-8"><label class="form-label">{% trans "Email" %}</label>{{ user_invite_form.email }}</div>
|
||||
<div class="col-md-4 d-flex align-items-end"><button class="btn btn-dark w-100">{% trans "Create invitation link" %}</button></div>
|
||||
</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 %}
|
||||
{% for invite in user_invites %}<div id="invite-{{ invite.id }}" class="border rounded p-3"><div class="d-flex justify-content-between gap-2"><strong>{{ invite.email }}</strong><form method="post" action="{% url 'delete_user_invite' invite.id %}" hx-post="{% url 'delete_user_invite' invite.id %}" hx-target="#invite-{{ invite.id }}" hx-swap="outerHTML">{% csrf_token %}<button class="btn btn-sm btn-outline-danger">{% trans "Delete" %}</button></form></div><code class="small user-select-all">{{ request.scheme }}://{{ request.get_host }}{% url 'accept_user_invite' invite.token %}</code></div>{% empty %}<span class="text-muted">{% trans "No open invitations." %}</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">
|
||||
|
||||
<div id="api" class="card item-card shadow-sm"><div class="card-body">
|
||||
<h2 class="h4 mb-1">{% trans "API keys" %}</h2>
|
||||
<p class="text-muted small mb-3">{% trans "Create API keys for integrations and optionally restrict them to tags." %}</p>
|
||||
<form method="post" class="vstack gap-3 mb-4">
|
||||
{% csrf_token %}<input type="hidden" name="action" value="apikey">
|
||||
<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>
|
||||
<button class="btn btn-dark align-self-start">{% trans "Generate API key" %}</button>
|
||||
</form>
|
||||
</div></div>
|
||||
<div class="card item-card shadow-sm"><div class="card-body">
|
||||
<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="border rounded p-3">
|
||||
<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">{% trans "Tags" %}: {% for tag in key.tags.all %}#{{ tag.name }} {% empty %}{% trans "all" %}{% endfor %}</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'settings' %}">← {% trans "Settings" %}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'team_list' %}">← {% trans "Teams" %}</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>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">{% trans "Teams" %}</h1>
|
||||
<div class="text-muted">{% trans "All teams you belong to." %}</div>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="/">← {% trans "back" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<section class="col-lg-4 order-lg-2">
|
||||
<div class="card item-card shadow-sm"><div class="card-body">
|
||||
<h2 class="h5 mb-3">{% trans "Create team" %}</h2>
|
||||
<form method="post" class="vstack gap-3">
|
||||
{% csrf_token %}
|
||||
<div><label class="form-label">{% trans "Team name" %}</label>{{ team_form.name }}</div>
|
||||
<div><label class="form-label">{% trans "Team tag" %}</label>{{ team_form.slug }}<div class="small text-muted mt-1">{% trans "This tag automatically turns items into team items, e.g. #my2dos." %}</div></div>
|
||||
<button class="btn btn-dark">{% trans "Create team" %}</button>
|
||||
</form>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="col-lg-8">
|
||||
<div class="vstack gap-3">
|
||||
{% for s in stats %}
|
||||
<article class="card item-card shadow-sm"><div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-2">
|
||||
<div>
|
||||
<h2 class="h5 mb-1"><a class="text-decoration-none" href="{% url 'team_detail' s.team.id %}">{{ s.team.name }}</a></h2>
|
||||
<div class="small text-muted">#{{ s.team.slug }} · {{ s.members_count }} {% trans "members" %}</div>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-outline-primary" href="{% url 'team_detail' s.team.id %}">{% trans "Open team" %}</a>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<span class="badge text-bg-light">{{ s.items_count }} {% trans "items" %}</span>
|
||||
<span class="badge text-bg-warning">{{ s.open_todos_count }} {% trans "open tasks" %}</span>
|
||||
</div>
|
||||
<div class="vstack gap-2">
|
||||
{% for item in s.recent_items %}
|
||||
<a class="border rounded p-2 text-decoration-none text-dark" href="{{ item.get_absolute_url }}">
|
||||
<span class="badge text-bg-light me-1">{{ item.get_kind_display }}</span>{{ item.content|truncatechars:120 }}
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="text-muted small">{% trans "No team items yet." %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div></article>
|
||||
{% empty %}
|
||||
<div class="card item-card shadow-sm"><div class="card-body text-muted">{% trans "You are not a member of any team yet." %}</div></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user