Spaces:
Sleeping
Sleeping
Commit ·
7ebc393
0
Parent(s):
Initial clean deploy to Hugging Face
Browse files- .gitignore +5 -0
- README.md +0 -0
- manage.py +23 -0
- requirements.txt +3 -0
- tasks/__init__.py +0 -0
- tasks/admin.py +6 -0
- tasks/apps.py +6 -0
- tasks/forms.py +13 -0
- tasks/migrations/0001_initial.py +31 -0
- tasks/migrations/0002_remove_task_updated_at_task_completed_at.py +22 -0
- tasks/migrations/__init__.py +0 -0
- tasks/models.py +22 -0
- tasks/templates/tasks/base.html +254 -0
- tasks/templates/tasks/delete_task.html +27 -0
- tasks/templates/tasks/list.html +77 -0
- tasks/templates/tasks/privacy.html +11 -0
- tasks/templates/tasks/support.html +21 -0
- tasks/templates/tasks/terms.html +16 -0
- tasks/templates/tasks/update_task.html +30 -0
- tasks/tests.py +3 -0
- tasks/urls.py +14 -0
- tasks/views.py +81 -0
- todo_project/__init__.py +0 -0
- todo_project/asgi.py +16 -0
- todo_project/settings.py +125 -0
- todo_project/urls.py +24 -0
- todo_project/wsgi.py +16 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
db.sqlite3
|
| 3 |
+
*.pyc
|
| 4 |
+
__pycache__/
|
| 5 |
+
.env
|
README.md
ADDED
|
Binary file (26 Bytes). View file
|
|
|
manage.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Django's command-line utility for administrative tasks."""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
"""Run administrative tasks."""
|
| 10 |
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo_project.settings")
|
| 11 |
+
try:
|
| 12 |
+
from django.core.management import execute_from_command_line
|
| 13 |
+
except ImportError as exc:
|
| 14 |
+
raise ImportError(
|
| 15 |
+
"Couldn't import Django. Are you sure it's installed and "
|
| 16 |
+
"available on your PYTHONPATH environment variable? Did you "
|
| 17 |
+
"forget to activate a virtual environment?"
|
| 18 |
+
) from exc
|
| 19 |
+
execute_from_command_line(sys.argv)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
if __name__ == "__main__":
|
| 23 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
django
|
| 2 |
+
django-todo
|
| 3 |
+
gunicorn
|
tasks/__init__.py
ADDED
|
File without changes
|
tasks/admin.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
|
| 3 |
+
# Register your models here.
|
| 4 |
+
from .models import Task
|
| 5 |
+
|
| 6 |
+
admin.site.register(Task)
|
tasks/apps.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class TasksConfig(AppConfig):
|
| 5 |
+
default_auto_field = "django.db.models.BigAutoField"
|
| 6 |
+
name = "tasks"
|
tasks/forms.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django import forms
|
| 2 |
+
from .models import Task
|
| 3 |
+
|
| 4 |
+
class TaskForm(forms.ModelForm):
|
| 5 |
+
class Meta:
|
| 6 |
+
model = Task
|
| 7 |
+
fields = ['title'] # <-- Changed this back to only the title!
|
| 8 |
+
widgets = {
|
| 9 |
+
'title': forms.TextInput(attrs={
|
| 10 |
+
'class': 'w-full px-4 py-3 rounded-xl bg-slate-50 border border-slate-200 focus:bg-white focus:ring-2 focus:ring-indigo-500 transition-all outline-none text-slate-700 placeholder-slate-400 shadow-sm',
|
| 11 |
+
'placeholder': 'What do you need to do?'
|
| 12 |
+
}),
|
| 13 |
+
}
|
tasks/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.11 on 2026-02-23 05:46
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
initial = True
|
| 9 |
+
|
| 10 |
+
dependencies = []
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.CreateModel(
|
| 14 |
+
name="Task",
|
| 15 |
+
fields=[
|
| 16 |
+
(
|
| 17 |
+
"id",
|
| 18 |
+
models.BigAutoField(
|
| 19 |
+
auto_created=True,
|
| 20 |
+
primary_key=True,
|
| 21 |
+
serialize=False,
|
| 22 |
+
verbose_name="ID",
|
| 23 |
+
),
|
| 24 |
+
),
|
| 25 |
+
("title", models.CharField(max_length=200)),
|
| 26 |
+
("completed", models.BooleanField(default=False)),
|
| 27 |
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
| 28 |
+
("updated_at", models.DateTimeField(auto_now=True)),
|
| 29 |
+
],
|
| 30 |
+
),
|
| 31 |
+
]
|
tasks/migrations/0002_remove_task_updated_at_task_completed_at.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.11 on 2026-02-23 06:20
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
("tasks", "0001_initial"),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.RemoveField(
|
| 14 |
+
model_name="task",
|
| 15 |
+
name="updated_at",
|
| 16 |
+
),
|
| 17 |
+
migrations.AddField(
|
| 18 |
+
model_name="task",
|
| 19 |
+
name="completed_at",
|
| 20 |
+
field=models.DateTimeField(blank=True, null=True),
|
| 21 |
+
),
|
| 22 |
+
]
|
tasks/migrations/__init__.py
ADDED
|
File without changes
|
tasks/models.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
|
| 3 |
+
# Create your models here.
|
| 4 |
+
from django.db import models
|
| 5 |
+
|
| 6 |
+
class Task(models.Model):
|
| 7 |
+
title = models.CharField(max_length=200)
|
| 8 |
+
completed = models.BooleanField(default=False)
|
| 9 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 10 |
+
# NEW: We allow this to be blank because new tasks aren't completed yet!
|
| 11 |
+
completed_at = models.DateTimeField(null=True, blank=True)
|
| 12 |
+
|
| 13 |
+
def __str__(self):
|
| 14 |
+
return self.title
|
| 15 |
+
|
| 16 |
+
from django import forms
|
| 17 |
+
from .models import Task
|
| 18 |
+
|
| 19 |
+
class TaskForm(forms.ModelForm):
|
| 20 |
+
class Meta:
|
| 21 |
+
model = Task
|
| 22 |
+
fields = ['title'] # We only need the user to type the title!
|
tasks/templates/tasks/base.html
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TaskPro | Intelligence in Organization</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<style>
|
| 9 |
+
:root { --bg-color: #f8fafc; --orb-1: #c084fc; --orb-2: #818cf8; --orb-3: #f472b6; }
|
| 10 |
+
body { background-color: var(--bg-color); transition: background-color 0.5s ease; }
|
| 11 |
+
.orb-1 { background-color: var(--orb-1); transition: background-color 0.5s ease; }
|
| 12 |
+
.orb-2 { background-color: var(--orb-2); transition: background-color 0.5s ease; }
|
| 13 |
+
.orb-3 { background-color: var(--orb-3); transition: background-color 0.5s ease; }
|
| 14 |
+
@keyframes float { 0% { transform: translate(0px, 0px) scale(1); } 33% { transform: translate(30px, -50px) scale(1.1); } 66% { transform: translate(-20px, 20px) scale(0.9); } 100% { transform: translate(0px, 0px) scale(1); } }
|
| 15 |
+
.animate-float { animation: float 10s infinite ease-in-out; }
|
| 16 |
+
.animation-delay-2000 { animation-delay: 2s; } .animation-delay-4000 { animation-delay: 4s; }
|
| 17 |
+
|
| 18 |
+
/* Smooth transition for dropdown */
|
| 19 |
+
.dropdown-animate { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); }
|
| 20 |
+
</style>
|
| 21 |
+
</head>
|
| 22 |
+
<body class="relative flex flex-col min-h-screen font-sans text-slate-800 selection:bg-indigo-200 selection:text-indigo-900 overflow-x-hidden">
|
| 23 |
+
|
| 24 |
+
<div id="custom-bg-layer" class="fixed inset-0 w-full h-full z-[-2] bg-cover bg-center bg-no-repeat transition-opacity duration-500 opacity-0"></div>
|
| 25 |
+
|
| 26 |
+
<div class="fixed inset-0 w-full h-full z-[-1]" id="orb-container">
|
| 27 |
+
<div class="orb-1 absolute top-[-10%] left-[-10%] w-96 h-96 rounded-full mix-blend-multiply filter blur-[100px] opacity-60 animate-float"></div>
|
| 28 |
+
<div class="orb-2 absolute top-[20%] right-[-5%] w-96 h-96 rounded-full mix-blend-multiply filter blur-[100px] opacity-60 animate-float animation-delay-2000"></div>
|
| 29 |
+
<div class="orb-3 absolute bottom-[-10%] left-[20%] w-96 h-96 rounded-full mix-blend-multiply filter blur-[100px] opacity-60 animate-float animation-delay-4000"></div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<header class="sticky top-0 z-50 bg-white/70 backdrop-blur-md border-b border-white/50 shadow-sm py-2 sm:py-0">
|
| 33 |
+
<div class="max-w-6xl mx-auto px-4 sm:px-6 min-h-[4.5rem] flex flex-wrap sm:flex-nowrap items-center justify-between gap-4">
|
| 34 |
+
|
| 35 |
+
<a href="/" class="flex items-center gap-2 cursor-pointer transform hover:scale-105 transition-transform shrink-0">
|
| 36 |
+
<div class="w-8 h-8 rounded-xl bg-gradient-to-br from-indigo-600 to-purple-600 flex items-center justify-center text-white shadow-md">
|
| 37 |
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
|
| 38 |
+
</div>
|
| 39 |
+
<span class="text-xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-indigo-700 to-purple-700 tracking-tight">TaskPro</span>
|
| 40 |
+
</a>
|
| 41 |
+
|
| 42 |
+
<div class="order-last sm:order-none w-full sm:w-auto flex-1 flex flex-col items-center justify-center max-w-lg mx-auto">
|
| 43 |
+
<div class="relative w-full">
|
| 44 |
+
<input type="text" id="searchInput" onkeyup="filterTasks()" placeholder="Search your tasks..." class="w-full pl-10 pr-4 py-2 rounded-full bg-white/60 border border-white focus:bg-white focus:ring-2 focus:ring-indigo-400 outline-none transition-all text-sm shadow-sm placeholder-slate-400">
|
| 45 |
+
<svg class="w-4 h-4 absolute left-4 top-2.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="text-[10px] font-bold text-slate-500 mt-1.5 uppercase tracking-widest" id="current-date-display">
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="flex items-center gap-2 sm:gap-4 shrink-0">
|
| 52 |
+
|
| 53 |
+
<button class="relative p-2 text-slate-400 hover:text-indigo-600 transition-colors transform hover:scale-110 group">
|
| 54 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
| 55 |
+
{% if pending_count and pending_count > 0 %}
|
| 56 |
+
<span class="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[9px] font-bold text-white shadow-sm ring-2 ring-white animate-pulse">
|
| 57 |
+
{{ pending_count }}
|
| 58 |
+
</span>
|
| 59 |
+
{% endif %}
|
| 60 |
+
</button>
|
| 61 |
+
|
| 62 |
+
<button onclick="toggleThemeModal()" class="flex items-center gap-2 text-sm font-bold text-slate-600 bg-white/80 hover:bg-white px-3 sm:px-4 py-2 rounded-full shadow-sm border border-slate-200 transition-all hover:scale-105">
|
| 63 |
+
🎨 <span class="hidden sm:inline">Themes</span>
|
| 64 |
+
</button>
|
| 65 |
+
|
| 66 |
+
<div class="relative shrink-0" id="avatar-container">
|
| 67 |
+
<button onclick="toggleAvatarMenu()" class="w-10 h-10 rounded-full bg-slate-200 border-2 border-white shadow-sm overflow-hidden cursor-pointer hover:ring-2 hover:ring-indigo-400 transition-all flex items-center justify-center">
|
| 68 |
+
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix&backgroundColor=e2e8f0" alt="Profile" class="w-full h-full object-cover">
|
| 69 |
+
</button>
|
| 70 |
+
|
| 71 |
+
<div id="avatar-menu" class="dropdown-animate absolute right-0 mt-3 w-64 bg-white/90 backdrop-blur-xl rounded-2xl shadow-2xl border border-white hidden opacity-0 transform scale-95 origin-top-right z-[100] overflow-hidden">
|
| 72 |
+
<div class="p-5 bg-gradient-to-br from-indigo-50 to-purple-50 border-b border-slate-100">
|
| 73 |
+
<p class="text-sm font-extrabold text-slate-800">Ihta (IAR Soft)</p>
|
| 74 |
+
<p class="text-[11px] text-indigo-600 font-bold uppercase tracking-wide mt-0.5">AI Engineer @ Jabiru Labs</p>
|
| 75 |
+
</div>
|
| 76 |
+
<div class="p-2 space-y-1">
|
| 77 |
+
<a href="/support" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-bold text-slate-600 hover:bg-white hover:text-indigo-600 transition-all shadow-sm shadow-transparent hover:shadow-slate-200">
|
| 78 |
+
<span class="text-lg">⚙️</span> Settings
|
| 79 |
+
</a>
|
| 80 |
+
<a href="/privacy" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-bold text-slate-600 hover:bg-white hover:text-indigo-600 transition-all shadow-sm shadow-transparent hover:shadow-slate-200">
|
| 81 |
+
<span class="text-lg">🛡️</span> Privacy Control
|
| 82 |
+
</a>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="p-2 border-t border-slate-100 bg-slate-50/50">
|
| 85 |
+
<a href="{% url 'logout' %}" class="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-bold text-red-500 hover:bg-red-50 transition-colors">
|
| 86 |
+
<span class="text-lg">🚪</span> Sign Out
|
| 87 |
+
</a>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</header>
|
| 94 |
+
|
| 95 |
+
<div id="theme-modal" class="fixed inset-0 z-[100] bg-slate-900/40 backdrop-blur-sm hidden flex items-center justify-center transition-opacity opacity-0">
|
| 96 |
+
<div class="bg-white/90 backdrop-blur-xl rounded-3xl shadow-2xl p-8 max-w-md w-full mx-4 transform scale-95 transition-transform duration-300 border border-white" id="theme-modal-content">
|
| 97 |
+
<div class="flex justify-between items-center mb-6">
|
| 98 |
+
<h2 class="text-2xl font-extrabold text-slate-800">Choose Theme 🎨</h2>
|
| 99 |
+
<button onclick="toggleThemeModal()" class="text-slate-400 hover:text-red-500 transition-colors">
|
| 100 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
| 101 |
+
</button>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="grid grid-cols-2 gap-4 mb-6">
|
| 104 |
+
<button onclick="setTheme('default')" class="h-24 rounded-xl shadow-inner bg-slate-50 border-2 border-slate-200 hover:border-indigo-500 overflow-hidden relative group">
|
| 105 |
+
<div class="absolute inset-0 opacity-40 bg-gradient-to-br from-purple-400 via-indigo-400 to-pink-400 group-hover:opacity-60 transition-opacity"></div>
|
| 106 |
+
<span class="relative z-10 font-bold text-slate-800 bg-white/80 px-2 py-1 rounded-md text-sm">Lavender Glass</span>
|
| 107 |
+
</button>
|
| 108 |
+
<button onclick="setTheme('ocean')" class="h-24 rounded-xl shadow-inner bg-cyan-50 border-2 border-slate-200 hover:border-cyan-500 overflow-hidden relative group">
|
| 109 |
+
<div class="absolute inset-0 opacity-40 bg-gradient-to-br from-blue-400 via-teal-400 to-cyan-400 group-hover:opacity-60 transition-opacity"></div>
|
| 110 |
+
<span class="relative z-10 font-bold text-slate-800 bg-white/80 px-2 py-1 rounded-md text-sm">Ocean Breeze</span>
|
| 111 |
+
</button>
|
| 112 |
+
<button onclick="setTheme('sunset')" class="h-24 rounded-xl shadow-inner bg-orange-50 border-2 border-slate-200 hover:border-orange-500 overflow-hidden relative group">
|
| 113 |
+
<div class="absolute inset-0 opacity-40 bg-gradient-to-br from-orange-400 via-red-400 to-yellow-400 group-hover:opacity-60 transition-opacity"></div>
|
| 114 |
+
<span class="relative z-10 font-bold text-slate-800 bg-white/80 px-2 py-1 rounded-md text-sm">Sunset Glow</span>
|
| 115 |
+
</button>
|
| 116 |
+
<button onclick="setTheme('forest')" class="h-24 rounded-xl shadow-inner bg-green-50 border-2 border-slate-200 hover:border-emerald-500 overflow-hidden relative group">
|
| 117 |
+
<div class="absolute inset-0 opacity-40 bg-gradient-to-br from-green-400 via-emerald-400 to-teal-400 group-hover:opacity-60 transition-opacity"></div>
|
| 118 |
+
<span class="relative z-10 font-bold text-slate-800 bg-white/80 px-2 py-1 rounded-md text-sm">Enchanted Forest</span>
|
| 119 |
+
</button>
|
| 120 |
+
<button onclick="setTheme('midnight')" class="h-24 rounded-xl shadow-inner bg-slate-900 border-2 border-slate-700 hover:border-purple-500 overflow-hidden relative group col-span-2">
|
| 121 |
+
<div class="absolute inset-0 opacity-40 bg-gradient-to-br from-indigo-900 via-purple-900 to-slate-800 group-hover:opacity-60 transition-opacity"></div>
|
| 122 |
+
<span class="relative z-10 font-bold text-white bg-black/50 px-2 py-1 rounded-md text-sm">Midnight Dark</span>
|
| 123 |
+
</button>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="border-t border-slate-200 pt-6">
|
| 126 |
+
<label class="block text-sm font-bold text-slate-700 mb-2">Or Upload Custom Background 🖼️</label>
|
| 127 |
+
<input type="file" id="custom-image-upload" accept="image/*" class="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 cursor-pointer">
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<main class="flex-grow py-8 px-4 sm:py-12 relative z-10">
|
| 133 |
+
{% block content %}{% endblock %}
|
| 134 |
+
</main>
|
| 135 |
+
|
| 136 |
+
<footer class="bg-white/40 backdrop-blur-md border-t border-white/50 mt-auto relative z-10">
|
| 137 |
+
<div class="max-w-5xl mx-auto px-4 py-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
| 138 |
+
<p class="text-sm text-slate-600 font-medium">© 2026 TaskPro. Built by Ihta <span class="text-red-500">❤️</span> in Django.</p>
|
| 139 |
+
<div class="flex gap-5">
|
| 140 |
+
<a href="{% url 'privacy' %}" class="text-slate-500 hover:text-indigo-700 transition-colors font-medium text-sm">Privacy Policy</a>
|
| 141 |
+
<a href="{% url 'terms' %}" class="text-slate-500 hover:text-indigo-700 transition-colors font-medium text-sm">Terms of Service</a>
|
| 142 |
+
<a href="{% url 'support' %}" class="text-slate-500 hover:text-indigo-700 transition-colors font-medium text-sm">Support</a>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</footer>
|
| 146 |
+
|
| 147 |
+
<script>
|
| 148 |
+
// --- 1. AVATAR DROPDOWN ---
|
| 149 |
+
const avatarMenu = document.getElementById('avatar-menu');
|
| 150 |
+
function toggleAvatarMenu() {
|
| 151 |
+
if (avatarMenu.classList.contains('hidden')) {
|
| 152 |
+
avatarMenu.classList.remove('hidden');
|
| 153 |
+
setTimeout(() => { avatarMenu.classList.remove('opacity-0', 'scale-95'); }, 10);
|
| 154 |
+
} else {
|
| 155 |
+
avatarMenu.classList.add('opacity-0', 'scale-95');
|
| 156 |
+
setTimeout(() => { avatarMenu.classList.add('hidden'); }, 200);
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
window.addEventListener('click', (e) => {
|
| 160 |
+
if (!document.getElementById('avatar-container').contains(e.target)) {
|
| 161 |
+
avatarMenu.classList.add('opacity-0', 'scale-95');
|
| 162 |
+
setTimeout(() => { avatarMenu.classList.add('hidden'); }, 200);
|
| 163 |
+
}
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
// --- 2. DATE & PROGRESS ---
|
| 167 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 168 |
+
const dateOptions = { weekday: 'long', month: 'short', day: 'numeric' };
|
| 169 |
+
const today = new Date().toLocaleDateString('en-US', dateOptions);
|
| 170 |
+
const pendingCount = "{{ pending_count|default:'0' }}";
|
| 171 |
+
document.getElementById('current-date-display').innerText = `${today} • ${pendingCount} Tasks Left`;
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
// --- 3. LIVE SEARCH ---
|
| 175 |
+
function filterTasks() {
|
| 176 |
+
let input = document.getElementById('searchInput').value.toLowerCase();
|
| 177 |
+
let taskItems = document.querySelectorAll('.task-item');
|
| 178 |
+
taskItems.forEach(item => {
|
| 179 |
+
let title = item.querySelector('.task-title').innerText.toLowerCase();
|
| 180 |
+
item.style.display = title.includes(input) ? "" : "none";
|
| 181 |
+
});
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// --- 4. THEMES ---
|
| 185 |
+
const themes = {
|
| 186 |
+
'default': { bg: '#f8fafc', o1: '#c084fc', o2: '#818cf8', o3: '#f472b6' },
|
| 187 |
+
'ocean': { bg: '#ecfeff', o1: '#38bdf8', o2: '#2dd4bf', o3: '#22d3ee' },
|
| 188 |
+
'sunset': { bg: '#fff7ed', o1: '#fb923c', o2: '#f87171', o3: '#facc15' },
|
| 189 |
+
'forest': { bg: '#f0fdf4', o1: '#4ade80', o2: '#34d399', o3: '#2dd4bf' },
|
| 190 |
+
'midnight': { bg: '#0f172a', o1: '#312e81', o2: '#4c1d95', o3: '#1e293b' }
|
| 191 |
+
};
|
| 192 |
+
const modal = document.getElementById('theme-modal');
|
| 193 |
+
const modalContent = document.getElementById('theme-modal-content');
|
| 194 |
+
const bgLayer = document.getElementById('custom-bg-layer');
|
| 195 |
+
const orbContainer = document.getElementById('orb-container');
|
| 196 |
+
|
| 197 |
+
function toggleThemeModal() {
|
| 198 |
+
if (modal.classList.contains('hidden')) {
|
| 199 |
+
modal.classList.remove('hidden');
|
| 200 |
+
setTimeout(() => { modal.classList.remove('opacity-0'); modalContent.classList.remove('scale-95'); }, 10);
|
| 201 |
+
} else {
|
| 202 |
+
modal.classList.add('opacity-0', 'scale-95');
|
| 203 |
+
setTimeout(() => { modal.classList.add('hidden'); }, 300);
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
function setTheme(themeName) {
|
| 208 |
+
const t = themes[themeName];
|
| 209 |
+
document.documentElement.style.setProperty('--bg-color', t.bg);
|
| 210 |
+
document.documentElement.style.setProperty('--orb-1', t.o1);
|
| 211 |
+
document.documentElement.style.setProperty('--orb-2', t.o2);
|
| 212 |
+
document.documentElement.style.setProperty('--orb-3', t.o3);
|
| 213 |
+
bgLayer.classList.replace('opacity-100', 'opacity-0');
|
| 214 |
+
orbContainer.style.display = 'block';
|
| 215 |
+
localStorage.setItem('taskpro-theme', themeName);
|
| 216 |
+
localStorage.removeItem('taskpro-custom-img');
|
| 217 |
+
if (!modal.classList.contains('hidden')) toggleThemeModal();
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
document.getElementById('custom-image-upload').addEventListener('change', function(e) {
|
| 221 |
+
const file = e.target.files[0];
|
| 222 |
+
if (file) {
|
| 223 |
+
const reader = new FileReader();
|
| 224 |
+
reader.onload = (event) => {
|
| 225 |
+
const imgData = event.target.result;
|
| 226 |
+
bgLayer.style.backgroundImage = `url(${imgData})`;
|
| 227 |
+
bgLayer.classList.replace('opacity-0', 'opacity-100');
|
| 228 |
+
orbContainer.style.display = 'none';
|
| 229 |
+
localStorage.setItem('taskpro-custom-img', imgData);
|
| 230 |
+
localStorage.setItem('taskpro-theme', 'custom');
|
| 231 |
+
if (!modal.classList.contains('hidden')) toggleThemeModal();
|
| 232 |
+
};
|
| 233 |
+
reader.readAsDataURL(file);
|
| 234 |
+
}
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
window.onload = () => {
|
| 238 |
+
const savedTheme = localStorage.getItem('taskpro-theme');
|
| 239 |
+
const savedImage = localStorage.getItem('taskpro-custom-img');
|
| 240 |
+
if (savedTheme === 'custom' && savedImage) {
|
| 241 |
+
bgLayer.style.backgroundImage = `url(${savedImage})`;
|
| 242 |
+
bgLayer.classList.replace('opacity-0', 'opacity-100');
|
| 243 |
+
orbContainer.style.display = 'none';
|
| 244 |
+
} else if (savedTheme && themes[savedTheme]) {
|
| 245 |
+
const t = themes[savedTheme];
|
| 246 |
+
document.documentElement.style.setProperty('--bg-color', t.bg);
|
| 247 |
+
document.documentElement.style.setProperty('--orb-1', t.o1);
|
| 248 |
+
document.documentElement.style.setProperty('--orb-2', t.o2);
|
| 249 |
+
document.documentElement.style.setProperty('--orb-3', t.o3);
|
| 250 |
+
}
|
| 251 |
+
};
|
| 252 |
+
</script>
|
| 253 |
+
</body>
|
| 254 |
+
</html>
|
tasks/templates/tasks/delete_task.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'tasks/base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="w-full max-w-md mx-auto bg-white/70 backdrop-blur-2xl rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden p-8 text-center border-t-4 border-red-500">
|
| 5 |
+
|
| 6 |
+
<div class="w-16 h-16 bg-red-100 text-red-500 rounded-full flex items-center justify-center mx-auto mb-6 text-3xl shadow-inner">
|
| 7 |
+
🗑️
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
<h2 class="text-2xl font-extrabold text-slate-800 mb-2">Delete Task?</h2>
|
| 11 |
+
<p class="text-slate-500 text-sm mb-8 px-2 font-medium">
|
| 12 |
+
Are you sure you want to delete "<span class="font-bold text-slate-700 italic">{{ item }}</span>"? This action cannot be undone.
|
| 13 |
+
</p>
|
| 14 |
+
|
| 15 |
+
<form method="POST" action="">
|
| 16 |
+
{% csrf_token %}
|
| 17 |
+
<div class="flex gap-3">
|
| 18 |
+
<a href="/" class="flex-1 py-3 bg-white/50 hover:bg-white text-slate-700 font-bold rounded-xl transition-all border border-slate-100 shadow-sm">
|
| 19 |
+
Cancel
|
| 20 |
+
</a>
|
| 21 |
+
<button type="submit" class="flex-1 py-3 bg-red-500 hover:bg-red-600 text-white font-bold rounded-xl shadow-lg hover:shadow-red-500/30 transition-all transform hover:-translate-y-0.5 active:scale-95">
|
| 22 |
+
Yes, Delete
|
| 23 |
+
</button>
|
| 24 |
+
</div>
|
| 25 |
+
</form>
|
| 26 |
+
</div>
|
| 27 |
+
{% endblock %}
|
tasks/templates/tasks/list.html
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'tasks/base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-lg mx-auto bg-white/70 backdrop-blur-2xl rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden transition-all duration-300 border border-white">
|
| 5 |
+
|
| 6 |
+
<div class="bg-gradient-to-r from-indigo-600 via-purple-600 to-indigo-700 p-8 text-center relative overflow-hidden shadow-inner">
|
| 7 |
+
<div class="absolute top-0 left-0 w-full h-full bg-white opacity-10 pattern-dots"></div>
|
| 8 |
+
<h2 class="text-3xl font-extrabold text-white mb-2 relative z-10 tracking-tight">My Tasks</h2>
|
| 9 |
+
<p class="text-indigo-100 text-sm font-medium relative z-10">Stay organized, stay productive </p>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<div class="p-6 sm:p-8 border-b border-slate-100/50 bg-white/50">
|
| 13 |
+
<form method="POST" action="/" class="flex gap-3 relative">
|
| 14 |
+
{% csrf_token %}
|
| 15 |
+
<div class="flex-1">
|
| 16 |
+
{{ form.title }}
|
| 17 |
+
</div>
|
| 18 |
+
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-xl font-bold shadow-lg transition-all transform hover:-translate-y-0.5 active:scale-95">
|
| 19 |
+
Add
|
| 20 |
+
</button>
|
| 21 |
+
</form>
|
| 22 |
+
</div>
|
| 23 |
+
|
| 24 |
+
<div class="p-6 sm:p-8 space-y-4 bg-slate-50/30">
|
| 25 |
+
<h3 class="font-bold text-slate-500 uppercase tracking-wider text-xs mb-4 flex items-center gap-2">
|
| 26 |
+
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span> Pending Tasks
|
| 27 |
+
</h3>
|
| 28 |
+
|
| 29 |
+
{% for task in pending_tasks %}
|
| 30 |
+
<div class="task-item flex items-center justify-between p-4 bg-white/80 backdrop-blur-sm border border-white rounded-2xl shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all group">
|
| 31 |
+
<div class="flex items-center gap-4 overflow-hidden flex-1">
|
| 32 |
+
<a href="{% url 'complete_task' task.id %}" class="w-6 h-6 rounded-full border-2 border-slate-300 hover:border-emerald-500 hover:bg-emerald-50 flex items-center justify-center transition-all group/check shrink-0">
|
| 33 |
+
<svg class="w-3.5 h-3.5 text-emerald-500 opacity-0 group-hover/check:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
|
| 34 |
+
</a>
|
| 35 |
+
<div class="flex-1 truncate pr-2">
|
| 36 |
+
<p class="task-title text-slate-700 font-bold text-[17px] leading-tight truncate group-hover:text-indigo-600 transition-colors">{{ task.title }}</p>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="flex gap-2 shrink-0 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
| 40 |
+
<a href="{% url 'update_task' task.id %}" class="w-8 h-8 flex items-center justify-center bg-slate-100 hover:bg-indigo-50 text-slate-400 hover:text-indigo-600 rounded-lg transition-colors">✏️</a>
|
| 41 |
+
<a href="{% url 'delete_task' task.id %}" class="w-8 h-8 flex items-center justify-center bg-slate-100 hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-lg transition-colors">🗑️</a>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
{% empty %}
|
| 45 |
+
<div class="text-center py-6 px-4 border-2 border-dashed border-slate-300 rounded-2xl bg-white/40">
|
| 46 |
+
<p class="text-slate-500 text-sm font-medium">No pending tasks right now! </p>
|
| 47 |
+
</div>
|
| 48 |
+
{% endfor %}
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="p-6 sm:p-8 border-t border-slate-100/50 bg-white/60">
|
| 52 |
+
<h3 class="font-bold text-slate-500 uppercase tracking-wider text-xs mb-4 flex items-center gap-2">
|
| 53 |
+
<span class="w-2 h-2 rounded-full bg-emerald-500"></span> Completed History
|
| 54 |
+
</h3>
|
| 55 |
+
<div class="space-y-3">
|
| 56 |
+
{% for task in completed_tasks %}
|
| 57 |
+
<div class="task-item flex items-center justify-between p-3 bg-white/50 border border-white rounded-xl group shadow-sm">
|
| 58 |
+
<div class="flex items-center gap-3 overflow-hidden flex-1">
|
| 59 |
+
<div class="w-6 h-6 rounded-full bg-emerald-500 flex items-center justify-center shrink-0">
|
| 60 |
+
<svg class="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="truncate pr-2">
|
| 63 |
+
<p class="task-title text-slate-500 line-through font-semibold text-sm truncate">{{ task.title }}</p>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="flex gap-1 shrink-0 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
| 67 |
+
<a href="{% url 'undo_task' task.id %}" class="text-[11px] font-bold text-slate-500 hover:text-indigo-600 px-3 py-1.5 bg-white rounded-md shadow-sm border border-slate-100">Undo</a>
|
| 68 |
+
<a href="{% url 'delete_task' task.id %}" class="text-[11px] font-bold text-slate-500 hover:text-red-500 px-3 py-1.5 bg-white rounded-md shadow-sm border border-slate-100">Del</a>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
{% empty %}
|
| 72 |
+
<p class="text-slate-500 text-sm italic text-center py-2">Finish a task to see your history!</p>
|
| 73 |
+
{% endfor %}
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
{% endblock %}
|
tasks/templates/tasks/privacy.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'tasks/base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-3xl mx-auto bg-white/80 backdrop-blur-xl border border-white rounded-3xl shadow-xl p-8 sm:p-12">
|
| 5 |
+
<h1 class="text-3xl font-extrabold text-indigo-600 mb-6">Privacy Policy</h1>
|
| 6 |
+
<p class="text-slate-600 mb-4">This is a demo To-Do list app. We do not collect or sell your personal data.</p>
|
| 7 |
+
<a href="/" class="inline-block mt-8 font-bold text-white bg-indigo-600 hover:bg-indigo-700 px-6 py-3 rounded-xl transition-colors">
|
| 8 |
+
⬅️ Back to Dashboard
|
| 9 |
+
</a>
|
| 10 |
+
</div>
|
| 11 |
+
{% endblock %}
|
tasks/templates/tasks/support.html
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'tasks/base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-3xl mx-auto bg-white/80 backdrop-blur-xl border border-white rounded-3xl shadow-xl p-8 sm:p-12 text-center border-t-4 border-indigo-500">
|
| 5 |
+
<div class="text-5xl mb-4">👋</div>
|
| 6 |
+
<h1 class="text-3xl font-extrabold text-slate-800 mb-4">Need Help?</h1>
|
| 7 |
+
<p class="text-slate-600 mb-8 max-w-md mx-auto">
|
| 8 |
+
If you are experiencing issues with the To-Do app, feel free to reach out. We are always looking to squash bugs and improve the UX!
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
<a href="mailto:khanihtesham0332@gmail.com" class="inline-block font-bold text-white bg-indigo-600 hover:bg-indigo-700 px-8 py-4 rounded-xl shadow-lg transition-transform hover:-translate-y-1">
|
| 12 |
+
📧 Email Support
|
| 13 |
+
</a>
|
| 14 |
+
|
| 15 |
+
<div class="mt-12">
|
| 16 |
+
<a href="/" class="font-bold text-slate-500 hover:text-indigo-600 transition-colors">
|
| 17 |
+
⬅️ Back to Dashboard
|
| 18 |
+
</a>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
{% endblock %}
|
tasks/templates/tasks/terms.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'tasks/base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-3xl mx-auto bg-white/80 backdrop-blur-xl border border-white rounded-3xl shadow-xl p-8 sm:p-12">
|
| 5 |
+
<h1 class="text-3xl font-extrabold text-indigo-600 mb-6">Terms of Service</h1>
|
| 6 |
+
<p class="text-slate-600 mb-4">
|
| 7 |
+
Welcome to TaskPro, operated by IAR Soft. By using this application, you agree to organize your tasks efficiently and boost your daily productivity.
|
| 8 |
+
</p>
|
| 9 |
+
<p class="text-slate-600 mb-4">
|
| 10 |
+
These terms are for demonstration purposes as part of this portfolio project.
|
| 11 |
+
</p>
|
| 12 |
+
<a href="/" class="inline-block mt-8 font-bold text-slate-600 bg-white/50 hover:bg-white px-6 py-3 rounded-xl transition-colors shadow-sm">
|
| 13 |
+
⬅️ Back to Dashboard
|
| 14 |
+
</a>
|
| 15 |
+
</div>
|
| 16 |
+
{% endblock %}
|
tasks/templates/tasks/update_task.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'tasks/base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="w-full max-w-md mx-auto bg-white/70 backdrop-blur-2xl rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden p-8 border border-white">
|
| 5 |
+
|
| 6 |
+
<div class="mb-8 text-center">
|
| 7 |
+
<h2 class="text-2xl font-extrabold text-slate-800">Update Task ✏️</h2>
|
| 8 |
+
<p class="text-slate-500 text-sm mt-1 font-medium">Refine your plans</p>
|
| 9 |
+
</div>
|
| 10 |
+
|
| 11 |
+
<form method="POST" action="" class="space-y-6">
|
| 12 |
+
{% csrf_token %}
|
| 13 |
+
|
| 14 |
+
<div class="space-y-2">
|
| 15 |
+
<label class="text-sm font-bold text-slate-700 ml-1">Task Title</label>
|
| 16 |
+
{{ form.title }}
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div class="pt-4 flex gap-3">
|
| 20 |
+
<a href="/" class="flex-1 text-center py-3 bg-white/50 hover:bg-white text-slate-700 font-bold rounded-xl transition-all shadow-sm border border-slate-100">
|
| 21 |
+
Cancel
|
| 22 |
+
</a>
|
| 23 |
+
<button type="submit" class="flex-1 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-xl shadow-lg hover:shadow-indigo-500/30 transition-all transform hover:-translate-y-0.5 active:scale-95">
|
| 24 |
+
Save Changes
|
| 25 |
+
</button>
|
| 26 |
+
</div>
|
| 27 |
+
</form>
|
| 28 |
+
|
| 29 |
+
</div>
|
| 30 |
+
{% endblock %}
|
tasks/tests.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
|
| 3 |
+
# Create your tests here.
|
tasks/urls.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from . import views
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path('', views.index, name='list'), # type: ignore
|
| 6 |
+
path('update_task/<str:pk>/', views.updateTask, name='update_task'),
|
| 7 |
+
path('delete_task/<str:pk>/', views.deleteTask, name='delete_task'),
|
| 8 |
+
path('complete_task/<str:pk>/', views.completeTask, name='complete_task'),
|
| 9 |
+
path('undo_task/<str:pk>/', views.undoTask, name='undo_task'),
|
| 10 |
+
path('privacy/', views.privacyPolicy, name='privacy'),
|
| 11 |
+
path('terms/', views.termsOfService, name='terms'), # <-- New!
|
| 12 |
+
path('support/', views.support, name='support'), # <-- New!
|
| 13 |
+
path('logout/', views.logoutUser, name='logout'),
|
| 14 |
+
]
|
tasks/views.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.shortcuts import render, redirect
|
| 2 |
+
from django.utils import timezone
|
| 3 |
+
from .models import Task
|
| 4 |
+
from .forms import TaskForm
|
| 5 |
+
from django.contrib.auth import logout
|
| 6 |
+
# A quick helper function to grab the pending count for the header
|
| 7 |
+
def get_global_context():
|
| 8 |
+
return {
|
| 9 |
+
'pending_count': Task.objects.filter(completed=False).count()
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
def index(request):
|
| 13 |
+
pending_tasks = Task.objects.filter(completed=False).order_by('-created_at')
|
| 14 |
+
completed_tasks = Task.objects.filter(completed=True).order_by('-completed_at')
|
| 15 |
+
form = TaskForm()
|
| 16 |
+
|
| 17 |
+
if request.method == 'POST':
|
| 18 |
+
form = TaskForm(request.POST)
|
| 19 |
+
if form.is_valid():
|
| 20 |
+
form.save()
|
| 21 |
+
return redirect('/')
|
| 22 |
+
|
| 23 |
+
context = {'pending_tasks': pending_tasks, 'completed_tasks': completed_tasks, 'form': form}
|
| 24 |
+
context.update(get_global_context()) # Add the bell count!
|
| 25 |
+
return render(request, 'tasks/list.html', context)
|
| 26 |
+
|
| 27 |
+
def updateTask(request, pk):
|
| 28 |
+
task = Task.objects.get(id=pk)
|
| 29 |
+
form = TaskForm(instance=task)
|
| 30 |
+
|
| 31 |
+
if request.method == 'POST':
|
| 32 |
+
form = TaskForm(request.POST, instance=task)
|
| 33 |
+
if form.is_valid():
|
| 34 |
+
form.save()
|
| 35 |
+
return redirect('/')
|
| 36 |
+
|
| 37 |
+
context = {'form': form}
|
| 38 |
+
context.update(get_global_context())
|
| 39 |
+
return render(request, 'tasks/update_task.html', context)
|
| 40 |
+
|
| 41 |
+
def deleteTask(request, pk):
|
| 42 |
+
item = Task.objects.get(id=pk)
|
| 43 |
+
if request.method == 'POST':
|
| 44 |
+
item.delete()
|
| 45 |
+
return redirect('/')
|
| 46 |
+
|
| 47 |
+
context = {'item': item}
|
| 48 |
+
context.update(get_global_context())
|
| 49 |
+
return render(request, 'tasks/delete_task.html', context)
|
| 50 |
+
|
| 51 |
+
def completeTask(request, pk):
|
| 52 |
+
task = Task.objects.get(id=pk)
|
| 53 |
+
task.completed = True
|
| 54 |
+
task.completed_at = timezone.now()
|
| 55 |
+
task.save()
|
| 56 |
+
return redirect('/')
|
| 57 |
+
|
| 58 |
+
def undoTask(request, pk):
|
| 59 |
+
task = Task.objects.get(id=pk)
|
| 60 |
+
task.completed = False
|
| 61 |
+
task.completed_at = None
|
| 62 |
+
task.save()
|
| 63 |
+
return redirect('/')
|
| 64 |
+
|
| 65 |
+
# --- Static Pages ---
|
| 66 |
+
def privacyPolicy(request):
|
| 67 |
+
context = get_global_context()
|
| 68 |
+
return render(request, 'tasks/privacy.html', context)
|
| 69 |
+
|
| 70 |
+
def termsOfService(request):
|
| 71 |
+
context = get_global_context()
|
| 72 |
+
return render(request, 'tasks/terms.html', context)
|
| 73 |
+
|
| 74 |
+
def support(request):
|
| 75 |
+
context = get_global_context()
|
| 76 |
+
return render(request, 'tasks/support.html', context)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def logoutUser(request):
|
| 80 |
+
logout(request)
|
| 81 |
+
return redirect('/') # Or redirect to a login page if you have one
|
todo_project/__init__.py
ADDED
|
File without changes
|
todo_project/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for todo_project project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo_project.settings")
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
todo_project/settings.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Django settings for todo_project project.
|
| 3 |
+
|
| 4 |
+
Generated by 'django-admin startproject' using Django 5.2.11.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/topics/settings/
|
| 8 |
+
|
| 9 |
+
For the full list of settings and their values, see
|
| 10 |
+
https://docs.djangoproject.com/en/5.2/ref/settings/
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 16 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Quick-start development settings - unsuitable for production
|
| 20 |
+
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
| 21 |
+
|
| 22 |
+
# SECURITY WARNING: keep the secret key used in production secret!
|
| 23 |
+
SECRET_KEY = "django-insecure-=ob5++5d=xr84)fsaqj$2wexu_)b*mp56s&#$x1un^n4q_$c7$"
|
| 24 |
+
|
| 25 |
+
# SECURITY WARNING: don't run with debug turned on in production!
|
| 26 |
+
DEBUG = True
|
| 27 |
+
|
| 28 |
+
# Allow all hosts so Hugging Face can display the app
|
| 29 |
+
ALLOWED_HOSTS = ['*']
|
| 30 |
+
|
| 31 |
+
# Required for the styles (Tailwind CDN) to work through the HF frame
|
| 32 |
+
CSRF_TRUSTED_ORIGINS = ['https://*.hf.space']
|
| 33 |
+
# Application definition
|
| 34 |
+
|
| 35 |
+
INSTALLED_APPS = [
|
| 36 |
+
"django.contrib.admin",
|
| 37 |
+
"django.contrib.auth",
|
| 38 |
+
"django.contrib.contenttypes",
|
| 39 |
+
"django.contrib.sessions",
|
| 40 |
+
"django.contrib.messages",
|
| 41 |
+
"django.contrib.staticfiles",
|
| 42 |
+
'tasks',
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
MIDDLEWARE = [
|
| 46 |
+
"django.middleware.security.SecurityMiddleware",
|
| 47 |
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
| 48 |
+
"django.middleware.common.CommonMiddleware",
|
| 49 |
+
"django.middleware.csrf.CsrfViewMiddleware",
|
| 50 |
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
| 51 |
+
"django.contrib.messages.middleware.MessageMiddleware",
|
| 52 |
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
ROOT_URLCONF = "todo_project.urls"
|
| 56 |
+
|
| 57 |
+
TEMPLATES = [
|
| 58 |
+
{
|
| 59 |
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
| 60 |
+
"DIRS": [],
|
| 61 |
+
"APP_DIRS": True,
|
| 62 |
+
"OPTIONS": {
|
| 63 |
+
"context_processors": [
|
| 64 |
+
"django.template.context_processors.request",
|
| 65 |
+
"django.contrib.auth.context_processors.auth",
|
| 66 |
+
"django.contrib.messages.context_processors.messages",
|
| 67 |
+
],
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
WSGI_APPLICATION = "todo_project.wsgi.application"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Database
|
| 76 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
| 77 |
+
|
| 78 |
+
DATABASES = {
|
| 79 |
+
"default": {
|
| 80 |
+
"ENGINE": "django.db.backends.sqlite3",
|
| 81 |
+
"NAME": BASE_DIR / "db.sqlite3",
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# Password validation
|
| 87 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
| 88 |
+
|
| 89 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 90 |
+
{
|
| 91 |
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
| 101 |
+
},
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# Internationalization
|
| 106 |
+
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
| 107 |
+
|
| 108 |
+
LANGUAGE_CODE = "en-us"
|
| 109 |
+
|
| 110 |
+
TIME_ZONE = "UTC"
|
| 111 |
+
|
| 112 |
+
USE_I18N = True
|
| 113 |
+
|
| 114 |
+
USE_TZ = True
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# Static files (CSS, JavaScript, Images)
|
| 118 |
+
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
| 119 |
+
|
| 120 |
+
STATIC_URL = "static/"
|
| 121 |
+
|
| 122 |
+
# Default primary key field type
|
| 123 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
| 124 |
+
|
| 125 |
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
todo_project/urls.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
URL configuration for todo_project project.
|
| 3 |
+
|
| 4 |
+
The `urlpatterns` list routes URLs to views. For more information please see:
|
| 5 |
+
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
| 6 |
+
Examples:
|
| 7 |
+
Function views
|
| 8 |
+
1. Add an import: from my_app import views
|
| 9 |
+
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
| 10 |
+
Class-based views
|
| 11 |
+
1. Add an import: from other_app.views import Home
|
| 12 |
+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
| 13 |
+
Including another URLconf
|
| 14 |
+
1. Import the include() function: from django.urls import include, path
|
| 15 |
+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from django.contrib import admin
|
| 19 |
+
from django.urls import path , include
|
| 20 |
+
|
| 21 |
+
urlpatterns = [
|
| 22 |
+
path("admin/", admin.site.urls),
|
| 23 |
+
path('', include('tasks.urls')),
|
| 24 |
+
]
|
todo_project/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for todo_project project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo_project.settings")
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|