diff --git a/assets/css/custom.css b/assets/css/custom.css
index 789132e..024b070 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -1,403 +1,540 @@
-body {
- background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
- background-size: 400% 400%;
- animation: gradient 15s ease infinite;
- color: #212529;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- font-size: 14px;
- margin: 0;
- min-height: 100vh;
+:root {
+ --bg: #f6f8fa;
+ --surface: #ffffff;
+ --surface-muted: #f6f8fa;
+ --surface-soft: #fafff8;
+ --border: #d0d7de;
+ --border-strong: #8c959f;
+ --text: #1f2328;
+ --text-muted: #57606a;
+ --text-soft: #6e7781;
+ --accent: #2da44e;
+ --accent-dark: #1f883d;
+ --accent-contrast: #ffffff;
+ --accent-blue: #0969da;
+ --accent-blue-soft: #ddf4ff;
+ --nav-bg: #0d1117;
+ --nav-text: #f0f6fc;
+ --success: #1a7f37;
+ --danger: #cf222e;
+ --shadow: 0 1px 0 rgba(27, 31, 36, 0.04), 0 8px 24px rgba(140, 149, 159, 0.18);
+ --radius-sm: 10px;
+ --radius-md: 14px;
+ --radius-lg: 18px;
}
-.main-wrapper {
- display: flex;
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ background:
+ radial-gradient(circle at top left, rgba(45, 164, 78, 0.08), transparent 26%),
+ radial-gradient(circle at top right, rgba(9, 105, 218, 0.08), transparent 22%),
+ var(--bg);
+ color: var(--text);
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ text-rendering: optimizeLegibility;
+}
+
+.app-header {
+ background: rgba(13, 17, 23, 0.92);
+ backdrop-filter: blur(12px);
+ border-bottom-color: rgba(240, 246, 252, 0.08) !important;
+}
+
+.navbar {
+ min-height: 72px;
+}
+
+.brand-mark {
+ width: 36px;
+ height: 36px;
+ border-radius: 11px;
+ display: inline-flex;
align-items: center;
justify-content: center;
- min-height: 100vh;
- width: 100%;
- padding: 20px;
- box-sizing: border-box;
- position: relative;
- z-index: 1;
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-blue) 100%);
+ color: var(--accent-contrast);
+ font-size: 1rem;
+ box-shadow: 0 10px 22px rgba(9, 105, 218, 0.22);
}
-@keyframes gradient {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
+.brand-title {
+ color: var(--nav-text);
+ font-size: 0.96rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ line-height: 1.1;
}
-.chat-container {
- width: 100%;
- max-width: 600px;
- background: rgba(255, 255, 255, 0.85);
- border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 20px;
+.brand-subtitle {
+ color: rgba(240, 246, 252, 0.72);
+ font-size: 0.76rem;
+ line-height: 1.2;
+}
+
+.panel {
+ background: rgba(255, 255, 255, 0.94);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ padding: 1.25rem;
+}
+
+.panel-hero {
+ padding: 1.5rem;
+}
+
+.eyebrow {
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-size: 0.71rem;
+ font-weight: 700;
+ margin-bottom: 0.85rem;
+}
+
+.hero-title,
+.panel-title,
+.detail-title {
+ letter-spacing: -0.04em;
+}
+
+.hero-title {
+ font-size: clamp(1.9rem, 3vw, 2.6rem);
+ line-height: 1.05;
+ margin-bottom: 0.85rem;
+}
+
+.hero-copy {
+ color: var(--text-muted);
+ max-width: 48ch;
+ margin-bottom: 0;
+}
+
+.meta-chip,
+.priority-badge,
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.42rem 0.7rem;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ border: 1px solid var(--border);
+ background: var(--surface-muted);
+ color: var(--text);
+}
+
+.stat-card {
display: flex;
flex-direction: column;
- height: 85vh;
- box-shadow: 0 20px 40px rgba(0,0,0,0.2);
- backdrop-filter: blur(15px);
- -webkit-backdrop-filter: blur(15px);
+ justify-content: space-between;
+ min-height: 100%;
+}
+
+.stat-card {
+ position: relative;
overflow: hidden;
}
-.chat-header {
- padding: 1.5rem;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
- background: rgba(255, 255, 255, 0.5);
- font-weight: 700;
- font-size: 1.1rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
+.stat-card::before {
+ content: '';
+ position: absolute;
+ inset: 0 auto auto 0;
+ width: 100%;
+ height: 4px;
+ background: linear-gradient(90deg, var(--accent), var(--accent-blue));
}
-.chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 1.5rem;
+.stat-card-active::before {
+ background: linear-gradient(90deg, var(--accent-blue), #54aeff);
+}
+
+.stat-card-done::before {
+ background: linear-gradient(90deg, var(--accent), #7ee787);
+}
+
+.stat-card-urgent::before {
+ background: linear-gradient(90deg, #fb8500, #f2cc60);
+}
+
+.stat-label,
+.stat-note,
+.detail-label,
+.task-meta,
+.form-text {
+ color: var(--text-muted);
+}
+
+.stat-label,
+.detail-label {
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-weight: 700;
+}
+
+.stat-value {
+ font-size: clamp(1.8rem, 3vw, 2.35rem);
+ line-height: 1;
+ letter-spacing: -0.05em;
+ margin: 0.5rem 0 0.35rem;
+}
+
+.stat-note {
+ font-size: 0.85rem;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.panel-header-stack {
+ align-items: stretch;
+}
+
+.panel-title {
+ font-size: 1.15rem;
+ font-weight: 700;
+}
+
+.form-control,
+.form-select,
+.input-group-text,
+.btn,
+.dropdown-menu {
+ border-radius: var(--radius-sm);
+}
+
+.form-control,
+.form-select,
+.input-group-text {
+ border-color: var(--border);
+ min-height: 46px;
+ background: #fff;
+}
+
+.form-control:focus,
+.form-select:focus {
+ border-color: var(--accent-blue);
+ box-shadow: 0 0 0 0.2rem rgba(9, 105, 218, 0.14);
+}
+
+.btn {
+ font-weight: 600;
+ box-shadow: none !important;
+}
+
+.btn-dark {
+ background: linear-gradient(180deg, #3fb950 0%, var(--accent) 100%);
+ border-color: var(--accent-dark);
+ color: var(--accent-contrast);
+}
+
+.btn-dark:hover,
+.btn-dark:focus {
+ background: linear-gradient(180deg, #2ea043 0%, var(--accent-dark) 100%);
+ border-color: #1a7f37;
+ color: var(--accent-contrast);
+}
+
+.btn-light {
+ background: #f6f8fa;
+ border-color: var(--border);
+ color: var(--text);
+}
+
+.btn-light:hover,
+.btn-light:focus {
+ background: #eef2f6;
+ border-color: var(--border-strong);
+ color: var(--text);
+}
+
+.filters-grid {
+ display: grid;
+ gap: 0.8rem;
+}
+
+.btn-filter {
+ border: 1px solid var(--border);
+ background: var(--surface-muted);
+ color: var(--text);
+ padding-inline: 0.9rem;
+}
+
+.btn-filter:hover {
+ border-color: var(--accent-blue);
+ color: var(--accent-blue);
+}
+
+.btn-check:checked + .btn-filter {
+ background: var(--accent-blue-soft);
+ color: var(--accent-blue);
+ border-color: #b6e3ff;
+}
+
+.sort-select {
+ min-width: 180px;
+}
+
+.task-list {
+ max-height: 70vh;
+ overflow: auto;
+ padding-right: 0.25rem;
+}
+
+.task-item {
+ position: relative;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 1rem;
+ background: #fff;
+ cursor: pointer;
+ transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease, background-color 0.18s ease;
+}
+
+.task-item:focus-visible {
+ outline: none;
+ border-color: var(--accent-blue);
+ box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.16);
+}
+
+.task-item:hover {
+ border-color: var(--accent-blue);
+ box-shadow: 0 12px 24px rgba(9, 105, 218, 0.08);
+ background: #fbfdff;
+ transform: translateY(-1px);
+}
+
+.task-item-active {
+ border-color: var(--accent-blue);
+ box-shadow: inset 3px 0 0 var(--accent-blue), 0 0 0 1px rgba(9, 105, 218, 0.08);
+ background: #f9fcff;
+}
+
+.task-item-done {
+ background: #f6fff8;
+}
+
+.check-button {
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ border: 1px solid var(--border);
+ background: #fff;
+ color: var(--text-muted);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.05rem;
+}
+
+.check-button:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ background: #f6fff8;
+}
+
+.task-title-link {
+ font-weight: 700;
+ color: var(--text);
+ text-decoration: none;
+ font-size: 0.97rem;
+}
+
+.task-title-link:hover {
+ color: var(--accent-blue);
+}
+
+.task-snippet,
+.detail-description {
+ color: var(--text-muted);
+ line-height: 1.55;
+}
+
+.task-item-done .task-title-link,
+.task-item-done .task-snippet {
+ opacity: 0.72;
+}
+
+.task-meta {
+ font-size: 0.78rem;
+}
+
+.priority-high,
+.priority-medium,
+.priority-low,
+.status-active,
+.status-done {
+ border: 1px solid var(--border);
+}
+
+.priority-high {
+ background: #fef2f2;
+ color: #991b1b;
+ border-color: #fecaca;
+}
+
+.priority-medium {
+ background: #fff8c5;
+ color: #9a6700;
+ border-color: #eedb85;
+}
+
+.priority-low {
+ background: #dafbe1;
+ color: #1a7f37;
+ border-color: #aceebb;
+}
+
+.status-active {
+ background: var(--accent-blue-soft);
+ color: var(--accent-blue);
+ border-color: #b6e3ff;
+}
+
+.status-done {
+ background: #dafbe1;
+ color: var(--success);
+ border-color: #aceebb;
+}
+
+.icon-button {
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.detail-panel {
display: flex;
flex-direction: column;
- gap: 1.25rem;
}
-/* Custom Scrollbar */
+.detail-stack {
+ height: 100%;
+}
+
+.detail-title {
+ font-size: 1.35rem;
+ margin-bottom: 0.7rem;
+}
+
+.detail-block {
+ padding: 0.9rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ background: linear-gradient(180deg, #ffffff 0%, #f6f8fa 100%);
+}
+
+.detail-value {
+ margin-top: 0.3rem;
+ font-weight: 600;
+}
+
+.empty-state {
+ border: 1px dashed var(--border-strong);
+ border-radius: var(--radius-md);
+ background: linear-gradient(180deg, #ffffff 0%, #f6f8fa 100%);
+ padding: 1.25rem;
+}
+
+.empty-icon {
+ width: 54px;
+ height: 54px;
+ margin-inline: auto;
+ border-radius: 50%;
+ background: #fff;
+ border: 1px solid var(--border);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.35rem;
+ color: var(--text-muted);
+}
+
+.toast {
+ border-radius: 12px;
+ box-shadow: 0 18px 36px rgba(13, 17, 23, 0.16);
+}
+
+.stretched-link-reset {
+ position: relative;
+ z-index: 2;
+}
+
+.toggle-form,
+.dropdown form {
+ position: relative;
+ z-index: 2;
+}
+
::-webkit-scrollbar {
- width: 6px;
+ width: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #d4d4d8;
+ border-radius: 999px;
+ border: 2px solid transparent;
+ background-clip: padding-box;
}
::-webkit-scrollbar-track {
background: transparent;
}
-::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.3);
- border-radius: 10px;
+@media (max-width: 1199.98px) {
+ .task-list {
+ max-height: none;
+ }
}
-::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.5);
+@media (max-width: 767.98px) {
+ .panel,
+ .panel-hero {
+ padding: 1rem;
+ }
+
+ .hero-title {
+ font-size: 1.7rem;
+ }
+
+ .panel-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .sort-select {
+ width: 100%;
+ }
}
-.message {
- max-width: 85%;
- padding: 0.85rem 1.1rem;
- border-radius: 16px;
- line-height: 1.5;
- font-size: 0.95rem;
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
- animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+.detail-panel {
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(246, 248, 250, 0.98) 100%);
}
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(20px) scale(0.95); }
- to { opacity: 1; transform: translateY(0) scale(1); }
+.navbar .btn-dark {
+ min-width: 132px;
}
-.message.visitor {
- align-self: flex-end;
- background: linear-gradient(135deg, #212529 0%, #343a40 100%);
- color: #fff;
- border-bottom-right-radius: 4px;
+.btn-outline-danger {
+ border-color: rgba(207, 34, 46, 0.28);
+ color: var(--danger);
}
-.message.bot {
- align-self: flex-start;
- background: #ffffff;
- color: #212529;
- border-bottom-left-radius: 4px;
+.btn-outline-danger:hover,
+.btn-outline-danger:focus {
+ background: var(--danger);
+ border-color: var(--danger);
}
-
-.chat-input-area {
- padding: 1.25rem;
- background: rgba(255, 255, 255, 0.5);
- border-top: 1px solid rgba(0, 0, 0, 0.05);
-}
-
-.chat-input-area form {
- display: flex;
- gap: 0.75rem;
-}
-
-.chat-input-area input {
- flex: 1;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 12px;
- padding: 0.75rem 1rem;
- outline: none;
- background: rgba(255, 255, 255, 0.9);
- transition: all 0.3s ease;
-}
-
-.chat-input-area input:focus {
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
-}
-
-.chat-input-area button {
- background: #212529;
- color: #fff;
- border: none;
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.3s ease;
-}
-
-.chat-input-area button:hover {
- background: #000;
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
-}
-
-/* Background Animations */
-.bg-animations {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 0;
- overflow: hidden;
- pointer-events: none;
-}
-
-.blob {
- position: absolute;
- width: 500px;
- height: 500px;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- filter: blur(80px);
- animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
-}
-
-.blob-1 {
- top: -10%;
- left: -10%;
- background: rgba(238, 119, 82, 0.4);
-}
-
-.blob-2 {
- bottom: -10%;
- right: -10%;
- background: rgba(35, 166, 213, 0.4);
- animation-delay: -7s;
- width: 600px;
- height: 600px;
-}
-
-.blob-3 {
- top: 40%;
- left: 30%;
- background: rgba(231, 60, 126, 0.3);
- animation-delay: -14s;
- width: 450px;
- height: 450px;
-}
-
-@keyframes move {
- 0% { transform: translate(0, 0) rotate(0deg) scale(1); }
- 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
- 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
- 100% { transform: translate(0, 0) rotate(360deg) scale(1); }
-}
-
-.header-link {
- font-size: 14px;
- color: #fff;
- text-decoration: none;
- background: rgba(0, 0, 0, 0.2);
- padding: 0.5rem 1rem;
- border-radius: 8px;
- transition: all 0.3s ease;
-}
-
-.header-link:hover {
- background: rgba(0, 0, 0, 0.4);
- text-decoration: none;
-}
-
-/* Admin Styles */
-.admin-container {
- max-width: 900px;
- margin: 3rem auto;
- padding: 2.5rem;
- background: rgba(255, 255, 255, 0.85);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-radius: 24px;
- box-shadow: 0 20px 50px rgba(0,0,0,0.15);
- border: 1px solid rgba(255, 255, 255, 0.4);
- position: relative;
- z-index: 1;
-}
-
-.admin-container h1 {
- margin-top: 0;
- color: #212529;
- font-weight: 800;
-}
-
-.table {
- width: 100%;
- border-collapse: separate;
- border-spacing: 0 8px;
- margin-top: 1.5rem;
-}
-
-.table th {
- background: transparent;
- border: none;
- padding: 1rem;
- color: #6c757d;
- font-weight: 600;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 1px;
-}
-
-.table td {
- background: #fff;
- padding: 1rem;
- border: none;
-}
-
-.table tr td:first-child { border-radius: 12px 0 0 12px; }
-.table tr td:last-child { border-radius: 0 12px 12px 0; }
-
-.form-group {
- margin-bottom: 1.25rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- font-size: 0.9rem;
-}
-
-.form-control {
- width: 100%;
- padding: 0.75rem 1rem;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 12px;
- background: #fff;
- transition: all 0.3s ease;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- outline: none;
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
-}
-
-.header-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.header-links {
- display: flex;
- gap: 1rem;
-}
-
-.admin-card {
- background: rgba(255, 255, 255, 0.6);
- padding: 2rem;
- border-radius: 20px;
- border: 1px solid rgba(255, 255, 255, 0.5);
- margin-bottom: 2.5rem;
- box-shadow: 0 10px 30px rgba(0,0,0,0.05);
-}
-
-.admin-card h3 {
- margin-top: 0;
- margin-bottom: 1.5rem;
- font-weight: 700;
-}
-
-.btn-delete {
- background: #dc3545;
- color: white;
- border: none;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- cursor: pointer;
-}
-
-.btn-add {
- background: #212529;
- color: white;
- border: none;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- cursor: pointer;
- margin-top: 1rem;
-}
-
-.btn-save {
- background: #0088cc;
- color: white;
- border: none;
- padding: 0.8rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
- font-weight: 600;
- width: 100%;
- transition: all 0.3s ease;
-}
-
-.webhook-url {
- font-size: 0.85em;
- color: #555;
- margin-top: 0.5rem;
-}
-
-.history-table-container {
- overflow-x: auto;
- background: rgba(255, 255, 255, 0.4);
- padding: 1rem;
- border-radius: 12px;
- border: 1px solid rgba(255, 255, 255, 0.3);
-}
-
-.history-table {
- width: 100%;
-}
-
-.history-table-time {
- width: 15%;
- white-space: nowrap;
- font-size: 0.85em;
- color: #555;
-}
-
-.history-table-user {
- width: 35%;
- background: rgba(255, 255, 255, 0.3);
- border-radius: 8px;
- padding: 8px;
-}
-
-.history-table-ai {
- width: 50%;
- background: rgba(255, 255, 255, 0.5);
- border-radius: 8px;
- padding: 8px;
-}
-
-.no-messages {
- text-align: center;
- color: #777;
-}
\ No newline at end of file
diff --git a/assets/js/main.js b/assets/js/main.js
index d349598..d49a96f 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,39 +1,232 @@
document.addEventListener('DOMContentLoaded', () => {
- const chatForm = document.getElementById('chat-form');
- const chatInput = document.getElementById('chat-input');
- const chatMessages = document.getElementById('chat-messages');
+ const toastEl = document.getElementById('appToast');
+ if (toastEl && window.bootstrap) {
+ const toast = new bootstrap.Toast(toastEl);
+ toast.show();
+ }
- const appendMessage = (text, sender) => {
- const msgDiv = document.createElement('div');
- msgDiv.classList.add('message', sender);
- msgDiv.textContent = text;
- chatMessages.appendChild(msgDiv);
- chatMessages.scrollTop = chatMessages.scrollHeight;
+ document.querySelectorAll('.needs-validation').forEach((form) => {
+ form.addEventListener('submit', (event) => {
+ if (!form.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ form.classList.add('was-validated');
+ });
+ });
+
+ const activeItem = document.querySelector('.task-item-active');
+ if (activeItem && window.innerWidth < 1200) {
+ activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+
+ const taskList = document.querySelector('.task-list');
+ const detailContent = document.getElementById('taskDetailContent');
+
+ if (!taskList || !detailContent) {
+ return;
+ }
+
+ const detailPriorityBadge = document.getElementById('detailPriorityBadge');
+ const detailStatusBadge = document.getElementById('detailStatusBadge');
+ const detailTitle = document.getElementById('detailTitle');
+ const detailDescription = document.getElementById('detailDescription');
+ const detailCreated = document.getElementById('detailCreated');
+ const detailUpdated = document.getElementById('detailUpdated');
+ const detailCompleted = document.getElementById('detailCompleted');
+ const detailEditLink = document.getElementById('detailEditLink');
+ const detailToggleTaskId = document.getElementById('detailToggleTaskId');
+ const detailDeleteTaskId = document.getElementById('detailDeleteTaskId');
+ const detailToggleIcon = document.getElementById('detailToggleIcon');
+ const detailToggleText = document.getElementById('detailToggleText');
+ const taskEditorForm = document.getElementById('taskEditorForm');
+ const taskFormAction = document.getElementById('taskFormAction');
+ const taskFormTaskId = document.getElementById('taskFormTaskId');
+ const taskFormSelectedTask = document.getElementById('taskFormSelectedTask');
+ const taskFormTitle = document.getElementById('taskFormTitle');
+ const taskFormEyebrow = document.getElementById('taskFormEyebrow');
+ const taskFormCancelLink = document.getElementById('taskFormCancelLink');
+ const taskTitleInput = document.getElementById('title');
+ const taskDescriptionInput = document.getElementById('description');
+ const taskPriorityInput = document.getElementById('priority');
+ const isEditingMode = taskEditorForm?.dataset.formMode === 'update';
+
+ const syncActiveState = (selectedItem) => {
+ document.querySelectorAll('.task-item').forEach((item) => {
+ const isActive = item === selectedItem;
+ item.classList.toggle('task-item-active', isActive);
+ item.setAttribute('aria-pressed', isActive ? 'true' : 'false');
+ });
};
- chatForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- const message = chatInput.value.trim();
- if (!message) return;
+ const syncEditForm = (taskItem) => {
+ if (!isEditingMode || !taskItem) {
+ return;
+ }
- appendMessage(message, 'visitor');
- chatInput.value = '';
+ if (taskFormAction) {
+ taskFormAction.value = 'update';
+ }
- try {
- const response = await fetch('api/chat.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
- const data = await response.json();
-
- // Artificial delay for realism
- setTimeout(() => {
- appendMessage(data.reply, 'bot');
- }, 500);
- } catch (error) {
- console.error('Error:', error);
- appendMessage("Sorry, something went wrong. Please try again.", 'bot');
+ if (taskFormTaskId) {
+ taskFormTaskId.value = taskItem.dataset.taskId || '';
+ }
+
+ if (taskFormSelectedTask) {
+ taskFormSelectedTask.value = taskItem.dataset.taskId || '';
+ }
+
+ if (taskTitleInput) {
+ taskTitleInput.value = taskItem.dataset.taskTitle || '';
+ }
+
+ if (taskDescriptionInput) {
+ taskDescriptionInput.value = taskItem.dataset.taskDescriptionRaw || '';
+ }
+
+ if (taskPriorityInput) {
+ taskPriorityInput.value = taskItem.dataset.taskPriorityValue || 'medium';
+ }
+
+ if (taskFormTitle) {
+ taskFormTitle.textContent = 'Обновить задачу';
+ }
+
+ if (taskFormEyebrow) {
+ taskFormEyebrow.textContent = 'Редактирование';
+ }
+
+ if (taskFormCancelLink) {
+ const cancelUrl = new URL(taskItem.dataset.taskEditUrl || 'index.php', window.location.origin);
+ cancelUrl.searchParams.delete('edit');
+ taskFormCancelLink.href = `${cancelUrl.pathname}${cancelUrl.search}${cancelUrl.hash}`;
+ }
+ };
+
+ const updateHistory = (taskId) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set('task', taskId);
+
+ if (isEditingMode) {
+ url.searchParams.set('edit', taskId);
+ } else {
+ url.searchParams.delete('edit');
+ }
+
+ history.replaceState({ taskId }, '', url);
+ };
+
+ const selectTask = (taskItem, shouldSyncHistory = true) => {
+ if (!taskItem) {
+ return;
+ }
+
+ syncActiveState(taskItem);
+ detailContent.dataset.selectedTaskId = taskItem.dataset.taskId || '';
+
+ if (detailPriorityBadge) {
+ detailPriorityBadge.className = `priority-badge ${taskItem.dataset.taskPriorityClass || ''}`.trim();
+ detailPriorityBadge.textContent = taskItem.dataset.taskPriorityLabel || '';
+ }
+
+ if (detailStatusBadge) {
+ detailStatusBadge.className = `status-badge ${taskItem.dataset.taskStatusClass || ''}`.trim();
+ detailStatusBadge.textContent = taskItem.dataset.taskStatusLabel || '';
+ }
+
+ if (detailTitle) {
+ detailTitle.textContent = taskItem.dataset.taskTitle || '';
+ }
+
+ if (detailDescription) {
+ detailDescription.textContent = taskItem.dataset.taskDescription || '';
+ }
+
+ if (detailCreated) {
+ detailCreated.textContent = taskItem.dataset.taskCreatedLabel || '';
+ }
+
+ if (detailUpdated) {
+ detailUpdated.textContent = taskItem.dataset.taskUpdatedLabel || '';
+ }
+
+ if (detailCompleted) {
+ detailCompleted.textContent = taskItem.dataset.taskCompletedLabel || '';
+ }
+
+ if (detailEditLink) {
+ detailEditLink.href = taskItem.dataset.taskEditUrl || 'index.php';
+ }
+
+ if (detailToggleTaskId) {
+ detailToggleTaskId.value = taskItem.dataset.taskId || '';
+ }
+
+ if (detailDeleteTaskId) {
+ detailDeleteTaskId.value = taskItem.dataset.taskId || '';
+ }
+
+ if (detailToggleIcon) {
+ detailToggleIcon.className = `bi ${taskItem.dataset.taskToggleIcon || 'bi-check2'} me-2`;
+ }
+
+ if (detailToggleText) {
+ detailToggleText.textContent = taskItem.dataset.taskToggleLabel || 'Отметить выполненной';
+ }
+
+ syncEditForm(taskItem);
+
+ if (shouldSyncHistory && taskItem.dataset.taskId) {
+ updateHistory(taskItem.dataset.taskId);
+ }
+ };
+
+ taskList.addEventListener('click', (event) => {
+ const taskItem = event.target.closest('.task-item');
+ if (!taskItem) {
+ return;
+ }
+
+ if (event.target.closest('form') || event.target.closest('[data-bs-toggle="dropdown"]') || event.target.closest('.dropdown-item-edit')) {
+ return;
+ }
+
+ if (event.target.closest('.dropdown-menu') && !event.target.closest('.task-open-link')) {
+ return;
+ }
+
+ const clickedLink = event.target.closest('a');
+ if (clickedLink && !event.target.closest('.task-open-link')) {
+ return;
+ }
+
+ event.preventDefault();
+ selectTask(taskItem);
+ });
+
+ taskList.addEventListener('keydown', (event) => {
+ const taskItem = event.target.closest('.task-item');
+ if (!taskItem || event.target !== taskItem) {
+ return;
+ }
+
+ if (event.key !== 'Enter' && event.key !== ' ') {
+ return;
+ }
+
+ event.preventDefault();
+ selectTask(taskItem);
+ });
+
+ window.addEventListener('popstate', () => {
+ const taskId = new URL(window.location.href).searchParams.get('task');
+ if (!taskId) {
+ return;
+ }
+
+ const matchingItem = document.querySelector(`.task-item[data-task-id="${CSS.escape(taskId)}"]`);
+ if (matchingItem) {
+ selectTask(matchingItem, false);
}
});
});
diff --git a/cookies.txt b/cookies.txt
new file mode 100644
index 0000000..1662aca
--- /dev/null
+++ b/cookies.txt
@@ -0,0 +1,5 @@
+# Netscape HTTP Cookie File
+# https://curl.se/docs/http-cookies.html
+# This file was generated by libcurl! Edit at your own risk.
+
+127.0.0.1 FALSE / FALSE 0 PHPSESSID 8euaff5qvrd5g7nf2qa3vr4rbd
diff --git a/healthz.php b/healthz.php
new file mode 100644
index 0000000..9450059
--- /dev/null
+++ b/healthz.php
@@ -0,0 +1,14 @@
+query('SELECT 1');
+ http_response_code(200);
+ echo "OK\n";
+} catch (Throwable $e) {
+ http_response_code(500);
+ echo "DB_ERROR\n";
+}
diff --git a/index.php b/index.php
index 7205f3d..71a275d 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,696 @@
$value) {
+ if ($value === null || $value === '') {
+ continue;
+ }
+ $filtered[$key] = $value;
+ }
+
+ return $filtered ? ('?' . http_build_query($filtered)) : '';
+}
+
+function redirectWithState(array $params): void
+{
+ header('Location: index.php' . buildQuery($params));
+ exit;
+}
+
+function setFlash(string $type, string $message): void
+{
+ $_SESSION['flash'] = ['type' => $type, 'message' => $message];
+}
+
+function getFlash(): ?array
+{
+ if (!isset($_SESSION['flash'])) {
+ return null;
+ }
+
+ $flash = $_SESSION['flash'];
+ unset($_SESSION['flash']);
+ return $flash;
+}
+
+function ensureTodoTable(PDO $pdo): void
+{
+ $pdo->exec(
+ "CREATE TABLE IF NOT EXISTS todos (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(140) NOT NULL,
+ description TEXT NULL,
+ priority ENUM('low', 'medium', 'high') NOT NULL DEFAULT 'medium',
+ is_completed TINYINT(1) NOT NULL DEFAULT 0,
+ completed_at DATETIME NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_completed (is_completed),
+ INDEX idx_priority (priority),
+ INDEX idx_created_at (created_at)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+
+ $count = (int) $pdo->query('SELECT COUNT(*) FROM todos')->fetchColumn();
+ if ($count === 0) {
+ $seed = $pdo->prepare(
+ 'INSERT INTO todos (title, description, priority, is_completed, completed_at) VALUES (:title, :description, :priority, :is_completed, :completed_at)'
+ );
+
+ $demoTasks = [
+ [
+ 'title' => 'Подготовить список задач на неделю',
+ 'description' => 'Собрать важные личные и рабочие дела, чтобы всё было в одном месте.',
+ 'priority' => 'high',
+ 'is_completed' => 0,
+ 'completed_at' => null,
+ ],
+ [
+ 'title' => 'Разобрать входящие письма',
+ 'description' => 'Оставить только действительно нужные письма и отметить follow-up.',
+ 'priority' => 'medium',
+ 'is_completed' => 0,
+ 'completed_at' => null,
+ ],
+ [
+ 'title' => 'Забронировать тренировку',
+ 'description' => 'Выбрать время на этой неделе и подтвердить занятие.',
+ 'priority' => 'low',
+ 'is_completed' => 1,
+ 'completed_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
+ ],
+ ];
+
+ foreach ($demoTasks as $task) {
+ $seed->execute([
+ ':title' => $task['title'],
+ ':description' => $task['description'],
+ ':priority' => $task['priority'],
+ ':is_completed' => $task['is_completed'],
+ ':completed_at' => $task['completed_at'],
+ ]);
+ }
+ }
+}
+
+$pdo = db();
+ensureTodoTable($pdo);
+
+if (empty($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+}
+$csrfToken = $_SESSION['csrf_token'];
+
+$status = $_GET['status'] ?? 'all';
+$status = in_array($status, ['all', 'active', 'completed'], true) ? $status : 'all';
+$sort = $_GET['sort'] ?? 'newest';
+$sort = in_array($sort, ['newest', 'oldest', 'priority'], true) ? $sort : 'newest';
+$q = trim((string) ($_GET['q'] ?? ''));
+$selectedTaskId = filter_input(INPUT_GET, 'task', FILTER_VALIDATE_INT) ?: null;
+$editTaskId = filter_input(INPUT_GET, 'edit', FILTER_VALIDATE_INT) ?: null;
+$baseState = [
+ 'status' => $status,
+ 'sort' => $sort,
+ 'q' => $q,
+];
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $postedToken = $_POST['csrf_token'] ?? '';
+ $postStatus = $_POST['status'] ?? 'all';
+ $postStatus = in_array($postStatus, ['all', 'active', 'completed'], true) ? $postStatus : 'all';
+ $postSort = $_POST['sort'] ?? 'newest';
+ $postSort = in_array($postSort, ['newest', 'oldest', 'priority'], true) ? $postSort : 'newest';
+ $postQ = trim((string) ($_POST['q'] ?? ''));
+ $returnTask = filter_input(INPUT_POST, 'task', FILTER_VALIDATE_INT) ?: null;
+ $redirectState = ['status' => $postStatus, 'sort' => $postSort, 'q' => $postQ];
+ if ($returnTask) {
+ $redirectState['task'] = $returnTask;
+ }
+
+ if (!hash_equals($csrfToken, $postedToken)) {
+ setFlash('danger', 'Сессия устарела. Попробуйте ещё раз.');
+ redirectWithState($redirectState);
+ }
+
+ $action = $_POST['action'] ?? '';
+
+ try {
+ if ($action === 'create') {
+ $title = trim((string) ($_POST['title'] ?? ''));
+ $description = trim((string) ($_POST['description'] ?? ''));
+ $priority = $_POST['priority'] ?? 'medium';
+ $priority = in_array($priority, ['low', 'medium', 'high'], true) ? $priority : 'medium';
+
+ if ($title === '' || utf8Length($title) > 140) {
+ throw new RuntimeException('Название задачи обязательно и должно быть короче 140 символов.');
+ }
+
+ if (utf8Length($description) > 1000) {
+ throw new RuntimeException('Описание должно быть короче 1000 символов.');
+ }
+
+ $stmt = $pdo->prepare('INSERT INTO todos (title, description, priority) VALUES (:title, :description, :priority)');
+ $stmt->bindValue(':title', $title);
+ $stmt->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
+ $stmt->bindValue(':priority', $priority);
+ $stmt->execute();
+
+ $newId = (int) $pdo->lastInsertId();
+ setFlash('success', 'Задача добавлена.');
+ $redirectState['task'] = $newId;
+ redirectWithState($redirectState);
+ }
+
+ if ($action === 'update') {
+ $taskId = filter_input(INPUT_POST, 'task_id', FILTER_VALIDATE_INT);
+ $title = trim((string) ($_POST['title'] ?? ''));
+ $description = trim((string) ($_POST['description'] ?? ''));
+ $priority = $_POST['priority'] ?? 'medium';
+ $priority = in_array($priority, ['low', 'medium', 'high'], true) ? $priority : 'medium';
+
+ if (!$taskId) {
+ throw new RuntimeException('Не удалось определить задачу для редактирования.');
+ }
+ if ($title === '' || utf8Length($title) > 140) {
+ throw new RuntimeException('Название задачи обязательно и должно быть короче 140 символов.');
+ }
+ if (utf8Length($description) > 1000) {
+ throw new RuntimeException('Описание должно быть короче 1000 символов.');
+ }
+
+ $stmt = $pdo->prepare('UPDATE todos SET title = :title, description = :description, priority = :priority WHERE id = :id LIMIT 1');
+ $stmt->bindValue(':title', $title);
+ $stmt->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
+ $stmt->bindValue(':priority', $priority);
+ $stmt->bindValue(':id', $taskId, PDO::PARAM_INT);
+ $stmt->execute();
+
+ setFlash('success', 'Изменения сохранены.');
+ $redirectState['task'] = $taskId;
+ redirectWithState($redirectState);
+ }
+
+ if ($action === 'toggle') {
+ $taskId = filter_input(INPUT_POST, 'task_id', FILTER_VALIDATE_INT);
+ if (!$taskId) {
+ throw new RuntimeException('Не удалось определить задачу.');
+ }
+
+ $stmt = $pdo->prepare('SELECT is_completed FROM todos WHERE id = :id LIMIT 1');
+ $stmt->execute([':id' => $taskId]);
+ $task = $stmt->fetch();
+ if (!$task) {
+ throw new RuntimeException('Задача не найдена.');
+ }
+
+ $nextState = (int) !$task['is_completed'];
+ $update = $pdo->prepare('UPDATE todos SET is_completed = :is_completed, completed_at = :completed_at WHERE id = :id LIMIT 1');
+ $update->bindValue(':is_completed', $nextState, PDO::PARAM_INT);
+ $update->bindValue(':completed_at', $nextState ? date('Y-m-d H:i:s') : null, $nextState ? PDO::PARAM_STR : PDO::PARAM_NULL);
+ $update->bindValue(':id', $taskId, PDO::PARAM_INT);
+ $update->execute();
+
+ setFlash('success', $nextState ? 'Задача отмечена как выполненная.' : 'Задача снова активна.');
+ $redirectState['task'] = $taskId;
+ redirectWithState($redirectState);
+ }
+
+ if ($action === 'delete') {
+ $taskId = filter_input(INPUT_POST, 'task_id', FILTER_VALIDATE_INT);
+ if (!$taskId) {
+ throw new RuntimeException('Не удалось определить задачу.');
+ }
+
+ $stmt = $pdo->prepare('DELETE FROM todos WHERE id = :id LIMIT 1');
+ $stmt->bindValue(':id', $taskId, PDO::PARAM_INT);
+ $stmt->execute();
+
+ setFlash('success', 'Задача удалена.');
+ unset($redirectState['task']);
+ redirectWithState($redirectState);
+ }
+
+ setFlash('warning', 'Неизвестное действие.');
+ redirectWithState($redirectState);
+ } catch (Throwable $exception) {
+ setFlash('danger', $exception->getMessage());
+ redirectWithState($redirectState);
+ }
+}
+
+$where = [];
+$params = [];
+if ($status === 'active') {
+ $where[] = 'is_completed = 0';
+} elseif ($status === 'completed') {
+ $where[] = 'is_completed = 1';
+}
+if ($q !== '') {
+ $where[] = '(title LIKE :search OR description LIKE :search)';
+ $params[':search'] = '%' . $q . '%';
+}
+$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
+$orderSql = match ($sort) {
+ 'oldest' => 'ORDER BY created_at ASC, id ASC',
+ 'priority' => "ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END ASC, is_completed ASC, created_at DESC",
+ default => 'ORDER BY created_at DESC, id DESC',
+};
+
+$listStmt = $pdo->prepare("SELECT id, title, description, priority, is_completed, completed_at, created_at, updated_at FROM todos {$whereSql} {$orderSql}");
+foreach ($params as $key => $value) {
+ $listStmt->bindValue($key, $value);
+}
+$listStmt->execute();
+$tasks = $listStmt->fetchAll();
+
+$stats = $pdo->query(
+ "SELECT
+ COUNT(*) AS total_count,
+ SUM(CASE WHEN is_completed = 0 THEN 1 ELSE 0 END) AS active_count,
+ SUM(CASE WHEN is_completed = 1 THEN 1 ELSE 0 END) AS completed_count,
+ SUM(CASE WHEN priority = 'high' AND is_completed = 0 THEN 1 ELSE 0 END) AS urgent_count
+ FROM todos"
+)->fetch() ?: ['total_count' => 0, 'active_count' => 0, 'completed_count' => 0, 'urgent_count' => 0];
+
+$selectedTask = null;
+if ($selectedTaskId) {
+ $detailStmt = $pdo->prepare('SELECT * FROM todos WHERE id = :id LIMIT 1');
+ $detailStmt->execute([':id' => $selectedTaskId]);
+ $selectedTask = $detailStmt->fetch() ?: null;
+}
+
+$editingTask = null;
+if ($editTaskId) {
+ if ($selectedTask && (int) $selectedTask['id'] === $editTaskId) {
+ $editingTask = $selectedTask;
+ } else {
+ $editStmt = $pdo->prepare('SELECT * FROM todos WHERE id = :id LIMIT 1');
+ $editStmt->execute([':id' => $editTaskId]);
+ $editingTask = $editStmt->fetch() ?: null;
+ }
+}
+
+if (!$selectedTask && !empty($tasks)) {
+ $selectedTask = $tasks[0];
+ $selectedTaskId = (int) $selectedTask['id'];
+}
+
+$flash = getFlash();
+$projectName = trim((string) ($_SERVER['PROJECT_NAME'] ?? '')) ?: 'Task Ledger';
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Personal todo application with database persistence and a modern focused interface.';
+$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+$pageTitle = $projectName . ' — Personal Todo';
+$assetVersion = (string) @filemtime(__DIR__ . '/assets/css/custom.css');
?>
-
+
-
-
- New Style
-
+
+
+ = h($pageTitle) ?>
+
-
-
-
-
-
-
+
+
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
-
-
-
Analyzing your requirements and generating your website…
-
- Loading…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
-
-
-
+
+
+
+
+
+
+ Всего задач
+ = h((string) ($stats['total_count'] ?? 0)) ?>
+ Личная база задач
+
+
+
+
+ Активные
+ = h((string) ($stats['active_count'] ?? 0)) ?>
+ В работе сейчас
+
+
+
+
+ Выполнены
+ = h((string) ($stats['completed_count'] ?? 0)) ?>
+ История прогресса
+
+
+
+
+ Высокий приоритет
+ = h((string) ($stats['urgent_count'] ?? 0)) ?>
+ Требуют внимания
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ничего не найдено
+
Измените фильтры или добавьте первую задачу, чтобы начать список.
+
Создать задачу
+
+
+
+
+ $task['id']]));
+ $editLink = buildQuery(array_merge($baseState, ['task' => $task['id'], 'edit' => $task['id']]));
+ ?>
+
+
+
+
+
+
= h($task['title']) ?>
+
= h(match ($task['priority']) {
+ 'high' => 'Высокий',
+ 'low' => 'Низкий',
+ default => 'Средний',
+ }) ?>
+
+
= h($task['description'] !== null && $task['description'] !== '' ? utf8Excerpt($task['description'], 120) : 'Без дополнительного описания.') ?>
+
+ = h(date('d M Y, H:i', strtotime($task['created_at']))) ?>
+ = $isDone ? 'Выполнено' : 'Активно' ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = h(match ($selectedTask['priority']) {
+ 'high' => 'Высокий приоритет',
+ 'low' => 'Низкий приоритет',
+ default => 'Средний приоритет',
+ }) ?>
+ = $selectedDone ? 'Выполнена' : 'Активна' ?>
+
+
= h($selectedTask['title']) ?>
+
= h($selectedTask['description'] ?: 'Описание не добавлено. Используйте это поле для контекста, критериев готовности или коротких заметок.') ?>
+
+
+
+
Создана
+
= h(date('d.m.Y H:i', strtotime($selectedTask['created_at']))) ?>
+
+
+
Последнее обновление
+
= h(date('d.m.Y H:i', strtotime($selectedTask['updated_at']))) ?>
+
+
+
Завершение
+
= $selectedTask['completed_at'] ? h(date('d.m.Y H:i', strtotime($selectedTask['completed_at']))) : 'Ещё в работе' ?>
+
+
+
+
+ Редактировать
+
+
+
+
+
+
+
+
+
Выберите задачу
+
После выбора здесь появятся детали и быстрые действия.
+
+
+
+
+
+
+
+
+
+
+
+
= h($flash['message']) ?>
+
+
+
+
+
+
+
+