Auto commit: 2026-04-16T11:25:44.037Z
This commit is contained in:
parent
e0e36aaad0
commit
3a44f34cf9
Binary file not shown.
@ -18,8 +18,10 @@ from django.contrib import admin
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("favicon.ico", RedirectView.as_view(url=settings.STATIC_URL + "images/favicon.svg", permanent=False)),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -14,12 +14,13 @@ class CategoryAdmin(admin.ModelAdmin):
|
|||||||
class MomentumEntryAdmin(admin.ModelAdmin):
|
class MomentumEntryAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"title",
|
"title",
|
||||||
|
"user",
|
||||||
"entry_date",
|
"entry_date",
|
||||||
"category",
|
"category",
|
||||||
"focus_score",
|
"focus_score",
|
||||||
"energy_score",
|
"energy_score",
|
||||||
"deep_work_minutes",
|
"deep_work_minutes",
|
||||||
)
|
)
|
||||||
list_filter = ("category", "entry_date")
|
list_filter = ("user", "category", "entry_date")
|
||||||
search_fields = ("title", "takeaway", "reflection")
|
search_fields = ("title", "takeaway", "reflection", "user__username")
|
||||||
date_hierarchy = "entry_date"
|
date_hierarchy = "entry_date"
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from .models import MomentumEntry
|
from .models import MomentumEntry
|
||||||
@ -64,3 +66,39 @@ class MomentumEntryForm(forms.ModelForm):
|
|||||||
if len(takeaway.split()) < 3:
|
if len(takeaway.split()) < 3:
|
||||||
raise forms.ValidationError("Write a short sentence with at least three words.")
|
raise forms.ValidationError("Write a short sentence with at least three words.")
|
||||||
return takeaway
|
return takeaway
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(AuthenticationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["username"].help_text = "Use the username you chose when creating your account."
|
||||||
|
self.fields["password"].help_text = "Enter the same password you used during sign up."
|
||||||
|
for field in self.fields.values():
|
||||||
|
field.widget.attrs["class"] = f"{field.widget.attrs.get('class', '')} form-control".strip()
|
||||||
|
field.widget.attrs.setdefault("autocomplete", "off")
|
||||||
|
|
||||||
|
class SignUpForm(UserCreationForm):
|
||||||
|
email = forms.EmailField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("username", "email", "password1", "password2")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["username"].help_text = "Pick a simple username for your private dashboard."
|
||||||
|
self.fields["email"].help_text = "Optional, but useful if you later add notifications."
|
||||||
|
self.fields["password1"].help_text = "Use at least 8 characters so your account is harder to guess."
|
||||||
|
self.fields["password2"].help_text = "Type the same password again to confirm it."
|
||||||
|
for name, field in self.fields.items():
|
||||||
|
field.widget.attrs["class"] = f"{field.widget.attrs.get('class', '')} form-control".strip()
|
||||||
|
if name == "username":
|
||||||
|
field.widget.attrs.setdefault("autofocus", True)
|
||||||
|
field.widget.attrs.setdefault("autocomplete", "off")
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
user = super().save(commit=False)
|
||||||
|
user.email = self.cleaned_data.get("email", "")
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|||||||
21
core/migrations/0003_momentumentry_user.py
Normal file
21
core/migrations/0003_momentumentry_user.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-04-16 11:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_seed_demo_data'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='momentumentry',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='momentum_entries', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -1,3 +1,4 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -17,6 +18,13 @@ class Category(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class MomentumEntry(models.Model):
|
class MomentumEntry(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="momentum_entries",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="entries")
|
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="entries")
|
||||||
title = models.CharField(max_length=120)
|
title = models.CharField(max_length=120)
|
||||||
entry_date = models.DateField()
|
entry_date = models.DateField()
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{% static 'images/favicon.svg' %}?v={{ deployment_timestamp }}">
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -6,6 +6,14 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="subpage-shell">
|
<div class="subpage-shell">
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
|
{% if messages %}
|
||||||
|
<div class="message-stack mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags|default:'info' }} custom-alert mb-0" role="alert">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if created %}
|
{% if created %}
|
||||||
<div class="alert custom-alert alert-success mb-4" role="alert">
|
<div class="alert custom-alert alert-success mb-4" role="alert">
|
||||||
Nice—your check-in was saved successfully. This detail page is the confirmation step of the MVP workflow.
|
Nice—your check-in was saved successfully. This detail page is the confirmation step of the MVP workflow.
|
||||||
@ -52,8 +60,12 @@
|
|||||||
<h2>Keep the loop going</h2>
|
<h2>Keep the loop going</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-3 mb-4">
|
<div class="d-grid gap-3 mb-4">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<a class="btn btn-accent" href="{% url 'home' %}#check-in">Log another day</a>
|
<a class="btn btn-accent" href="{% url 'home' %}#check-in">Log another day</a>
|
||||||
<a class="btn btn-ghost" href="{% url 'entry_list' %}">View all entries</a>
|
{% else %}
|
||||||
|
<a class="btn btn-accent" href="{% url 'signup' %}">Create your account</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="btn btn-ghost" href="{% url 'entry_list' %}">{% if is_demo_mode %}View demo entries{% else %}View all entries{% endif %}</a>
|
||||||
<a class="btn btn-ghost" href="/admin/">Open admin</a>
|
<a class="btn btn-ghost" href="/admin/">Open admin</a>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="h5">More in {{ entry.category.name }}</h3>
|
<h3 class="h5">More in {{ entry.category.name }}</h3>
|
||||||
|
|||||||
@ -6,15 +6,32 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="subpage-shell">
|
<div class="subpage-shell">
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
|
{% if messages %}
|
||||||
|
<div class="message-stack mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags|default:'info' }} custom-alert mb-0" role="alert">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="subpage-header glass-panel mb-4">
|
<div class="subpage-header glass-panel mb-4">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">History</span>
|
<span class="eyebrow">History</span>
|
||||||
<h1>All momentum entries</h1>
|
<h1>{% if is_demo_mode %}Demo momentum entries{% else %}Your momentum entries{% endif %}</h1>
|
||||||
<p>Filter by category and open any check-in for its confirmation/detail view.</p>
|
<p>{% if is_demo_mode %}Browse the seeded sample history, then create an account when you want private tracking.{% else %}Filter your private check-ins by category and open any one for its detail view.{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
<a class="btn btn-ghost" href="{% url 'home' %}">Back to dashboard</a>
|
<a class="btn btn-ghost" href="{% url 'home' %}">Back to dashboard</a>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<a class="btn btn-accent" href="{% url 'home' %}#check-in">New check-in</a>
|
<a class="btn btn-accent" href="{% url 'home' %}#check-in">New check-in</a>
|
||||||
|
<form method="post" action="{% url 'logout' %}" class="inline-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-ghost" type="submit">Log out</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-ghost" href="{% url 'login' %}">Log in</a>
|
||||||
|
<a class="btn btn-accent" href="{% url 'signup' %}">Create account</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -47,9 +64,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="glass-panel empty-state">
|
<div class="glass-panel empty-state">
|
||||||
<h2>No entries for this filter</h2>
|
<h2>{% if is_demo_mode %}No demo entries for this filter{% else %}No private entries for this filter{% endif %}</h2>
|
||||||
<p>Try a different category or create a new check-in from the dashboard.</p>
|
<p>{% if is_demo_mode %}Try a different category or create an account to start tracking your own days.{% else %}Try a different category or create a new check-in from the dashboard.{% endif %}</p>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<a class="btn btn-accent mt-3" href="{% url 'home' %}#check-in">Create an entry</a>
|
<a class="btn btn-accent mt-3" href="{% url 'home' %}#check-in">Create an entry</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-accent mt-3" href="{% url 'signup' %}">Create your account</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,9 +14,21 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="mainNav">
|
<div class="collapse navbar-collapse" id="mainNav">
|
||||||
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
|
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
|
||||||
<li class="nav-item"><a class="nav-link" href="#check-in">New check-in</a></li>
|
<li class="nav-item"><a class="nav-link" href="#check-in">{% if request.user.is_authenticated %}New check-in{% else %}Get started{% endif %}</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#weekly-trend">Weekly trend</a></li>
|
<li class="nav-item"><a class="nav-link" href="#weekly-trend">Weekly trend</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{% url 'entry_list' %}">All entries</a></li>
|
<li class="nav-item"><a class="nav-link" href="{% url 'entry_list' %}">{% if request.user.is_authenticated %}My entries{% else %}Demo entries{% endif %}</a></li>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<li class="nav-item"><span class="nav-user">{{ request.user.username }}</span></li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<form method="post" action="{% url 'logout' %}" class="nav-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="nav-link nav-pill nav-button">Log out</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Log in</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link nav-pill" href="{% url 'signup' %}">Sign up</a></li>
|
||||||
|
{% endif %}
|
||||||
<li class="nav-item"><a class="nav-link nav-pill" href="/admin/">Admin</a></li>
|
<li class="nav-item"><a class="nav-link nav-pill" href="/admin/">Admin</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -29,12 +41,23 @@
|
|||||||
<div class="container py-5 position-relative">
|
<div class="container py-5 position-relative">
|
||||||
<div class="row align-items-center g-5">
|
<div class="row align-items-center g-5">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<span class="eyebrow">Interesting Python project · daily tracker MVP</span>
|
<span class="eyebrow">{% if request.user.is_authenticated %}Private momentum tracker{% else %}Interesting Python project · try the demo{% endif %}</span>
|
||||||
<h1 class="hero-title">Track your momentum like a product, not a spreadsheet.</h1>
|
<h1 class="hero-title">Track your momentum like a product, not a spreadsheet.</h1>
|
||||||
<p class="hero-copy">Log a single daily check-in, watch your focus and energy trend over time, and turn a simple Django app into a polished personal dashboard you can actually share.</p>
|
<p class="hero-copy">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
Welcome back, {{ request.user.username }}. Your entries are now private to your account, so this dashboard reflects your own focus, energy, and small wins.
|
||||||
|
{% else %}
|
||||||
|
Explore the tracker with demo data first, then create a free account when you are ready to save private daily check-ins.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
<div class="hero-actions d-flex flex-wrap gap-3">
|
<div class="hero-actions d-flex flex-wrap gap-3">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<a class="btn btn-accent btn-lg" href="#check-in">Log today’s momentum</a>
|
<a class="btn btn-accent btn-lg" href="#check-in">Log today’s momentum</a>
|
||||||
<a class="btn btn-ghost btn-lg" href="{% url 'entry_list' %}">Browse history</a>
|
<a class="btn btn-ghost btn-lg" href="{% url 'entry_list' %}">Browse your history</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-accent btn-lg" href="{% url 'signup' %}">Create free account</a>
|
||||||
|
<a class="btn btn-ghost btn-lg" href="{% url 'entry_list' %}">Browse demo history</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta d-flex flex-wrap gap-4 mt-4">
|
<div class="hero-meta d-flex flex-wrap gap-4 mt-4">
|
||||||
<div>
|
<div>
|
||||||
@ -49,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="glass-panel insight-panel">
|
<div class="glass-panel insight-panel">
|
||||||
<div class="panel-label">30-day snapshot</div>
|
<div class="panel-label">{% if is_demo_mode %}Demo 30-day snapshot{% else %}Your 30-day snapshot{% endif %}</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<article class="metric-card">
|
<article class="metric-card">
|
||||||
@ -105,10 +128,17 @@
|
|||||||
<div class="col-xl-7">
|
<div class="col-xl-7">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<span class="eyebrow">Workflow widget</span>
|
<span class="eyebrow">Workflow widget</span>
|
||||||
<h2>Capture one meaningful check-in.</h2>
|
<h2>{% if request.user.is_authenticated %}Capture one meaningful check-in.{% else %}Create your account to start saving check-ins.{% endif %}</h2>
|
||||||
<p>Each entry becomes a usable artifact: you log the day, land on a confirmation detail view, and keep building a real momentum history.</p>
|
<p>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
Each entry becomes a private artifact: you log the day, land on a confirmation detail view, and build your own momentum history.
|
||||||
|
{% else %}
|
||||||
|
You can already browse the demo dashboard. Once you sign up, new entries will belong only to your account.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass-panel form-panel">
|
<div class="glass-panel form-panel">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<form method="post" novalidate>
|
<form method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
@ -132,6 +162,19 @@
|
|||||||
<span class="form-hint mb-0">Default categories are ready, and you can manage them later from Django Admin.</span>
|
<span class="form-hint mb-0">Default categories are ready, and you can manage them later from Django Admin.</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="auth-callout">
|
||||||
|
<div class="chip-row mb-3">
|
||||||
|
<span class="trend-chip">Private entries</span>
|
||||||
|
<span class="trend-chip">Login + signup ready</span>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4">This prevents your personal notes from mixing with the demo sample data. It is a common first step when an app starts storing user-specific information.</p>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<a class="btn btn-accent btn-lg" href="{% url 'signup' %}">Create account</a>
|
||||||
|
<a class="btn btn-ghost btn-lg" href="{% url 'login' %}">Log in</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-5">
|
<div class="col-xl-5">
|
||||||
@ -142,7 +185,7 @@
|
|||||||
<div class="stack-grid">
|
<div class="stack-grid">
|
||||||
<article class="glass-panel feature-card">
|
<article class="glass-panel feature-card">
|
||||||
<h3>Create</h3>
|
<h3>Create</h3>
|
||||||
<p>Submit a structured daily entry with validation, categories, and tangible metrics.</p>
|
<p>{% if request.user.is_authenticated %}Submit a structured daily entry with validation, categories, and personal ownership.{% else %}Create an account in a few seconds so the app can keep your data separate from demo content.{% endif %}</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="glass-panel feature-card">
|
<article class="glass-panel feature-card">
|
||||||
<h3>Confirm</h3>
|
<h3>Confirm</h3>
|
||||||
@ -163,9 +206,9 @@
|
|||||||
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
|
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
|
||||||
<div class="section-heading compact mb-0">
|
<div class="section-heading compact mb-0">
|
||||||
<span class="eyebrow">7-day insight</span>
|
<span class="eyebrow">7-day insight</span>
|
||||||
<h2>Focus and energy trend</h2>
|
<h2>{% if is_demo_mode %}Demo focus and energy trend{% else %}Your focus and energy trend{% endif %}</h2>
|
||||||
</div>
|
</div>
|
||||||
<a class="text-link" href="{% url 'entry_list' %}">Open full history</a>
|
<a class="text-link" href="{% url 'entry_list' %}">{% if is_demo_mode %}Open demo history{% else %}Open your history{% endif %}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass-panel chart-panel">
|
<div class="glass-panel chart-panel">
|
||||||
<div class="trend-chart">
|
<div class="trend-chart">
|
||||||
@ -190,9 +233,9 @@
|
|||||||
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
|
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
|
||||||
<div class="section-heading compact mb-0">
|
<div class="section-heading compact mb-0">
|
||||||
<span class="eyebrow">Recent check-ins</span>
|
<span class="eyebrow">Recent check-ins</span>
|
||||||
<h2>Momentum history preview</h2>
|
<h2>{% if is_demo_mode %}Demo momentum history preview{% else %}Your momentum history preview{% endif %}</h2>
|
||||||
</div>
|
</div>
|
||||||
<a class="text-link" href="{% url 'entry_list' %}">See every entry</a>
|
<a class="text-link" href="{% url 'entry_list' %}">{% if is_demo_mode %}See every demo entry{% else %}See every entry{% endif %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% if recent_entries %}
|
{% if recent_entries %}
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
@ -216,8 +259,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="glass-panel empty-state">
|
<div class="glass-panel empty-state">
|
||||||
<h3>No entries yet</h3>
|
<h3>{% if request.user.is_authenticated %}No private entries yet{% else %}No demo entries yet{% endif %}</h3>
|
||||||
<p>Start with one check-in above and this dashboard will instantly become your first useful Python product demo.</p>
|
<p>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
Start with one check-in above and this dashboard will instantly become your own useful Python product demo.
|
||||||
|
{% else %}
|
||||||
|
Create an account to start filling this dashboard with your own progress history.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
core/templates/core/signup.html
Normal file
58
core/templates/core/signup.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
{% block meta_description %}{{ meta_description }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="subpage-shell auth-shell">
|
||||||
|
<div class="container py-5">
|
||||||
|
{% if messages %}
|
||||||
|
<div class="message-stack mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags|default:'info' }} custom-alert mb-0" role="alert">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-7 col-xl-6">
|
||||||
|
<div class="glass-panel auth-card">
|
||||||
|
<span class="eyebrow">Create account</span>
|
||||||
|
<h1>Start your private tracker</h1>
|
||||||
|
<p>This is the simple user system behind the app: once you sign up, each new entry is stored under your account instead of the public demo sample.</p>
|
||||||
|
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ next_url }}">
|
||||||
|
<div class="row g-3">
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="col-12 {% if field.name == 'username' or field.name == 'email' %}col-md-6{% endif %}">
|
||||||
|
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-hint">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="invalid-feedback d-block mt-3">
|
||||||
|
{% for error in form.non_field_errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="d-flex flex-wrap gap-3 align-items-center mt-4">
|
||||||
|
<button type="submit" class="btn btn-accent btn-lg">Create account</button>
|
||||||
|
<a class="btn btn-ghost btn-lg" href="{% url 'login' %}{% if next_url %}?next={{ next_url|urlencode }}{% endif %}">I already have an account</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
58
core/templates/registration/login.html
Normal file
58
core/templates/registration/login.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title|default:"Log in | Momentum Atlas" }}{% endblock %}
|
||||||
|
{% block meta_description %}Log in to Momentum Atlas and continue your private daily tracking workflow.{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="subpage-shell auth-shell">
|
||||||
|
<div class="container py-5">
|
||||||
|
{% if messages %}
|
||||||
|
<div class="message-stack mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags|default:'info' }} custom-alert mb-0" role="alert">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6 col-xl-5">
|
||||||
|
<div class="glass-panel auth-card">
|
||||||
|
<span class="eyebrow">Account access</span>
|
||||||
|
<h1>Log in to your dashboard</h1>
|
||||||
|
<p>Once logged in, new check-ins belong to your account and only you will see them.</p>
|
||||||
|
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-hint">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="invalid-feedback d-block mb-3">
|
||||||
|
{% for error in form.non_field_errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||||
|
<button type="submit" class="btn btn-accent btn-lg w-100">Log in</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-footer">
|
||||||
|
<span>New here?</span>
|
||||||
|
<a class="text-link" href="{% url 'signup' %}{% if next %}?next={{ next|urlencode }}{% endif %}">Create your free account</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
15
core/urls.py
15
core/urls.py
@ -1,9 +1,22 @@
|
|||||||
|
from django.contrib.auth import views as auth_views
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import entry_detail, entry_list, home
|
from .forms import LoginForm
|
||||||
|
from .views import entry_detail, entry_list, home, signup
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
path("entries/", entry_list, name="entry_list"),
|
path("entries/", entry_list, name="entry_list"),
|
||||||
path("entries/<int:pk>/", entry_detail, name="entry_detail"),
|
path("entries/<int:pk>/", entry_detail, name="entry_detail"),
|
||||||
|
path(
|
||||||
|
"login/",
|
||||||
|
auth_views.LoginView.as_view(
|
||||||
|
template_name="registration/login.html",
|
||||||
|
redirect_authenticated_user=True,
|
||||||
|
authentication_form=LoginForm,
|
||||||
|
),
|
||||||
|
name="login",
|
||||||
|
),
|
||||||
|
path("logout/", auth_views.LogoutView.as_view(next_page="home"), name="logout"),
|
||||||
|
path("signup/", signup, name="signup"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -4,12 +4,14 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django import get_version as django_version
|
from django import get_version as django_version
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db.models import Avg, Count, Sum
|
from django.contrib.auth import login
|
||||||
|
from django.db.models import Avg, Count, Q, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from .forms import MomentumEntryForm
|
from .forms import MomentumEntryForm, SignUpForm
|
||||||
from .models import Category, MomentumEntry
|
from .models import Category, MomentumEntry
|
||||||
|
|
||||||
|
|
||||||
@ -17,6 +19,21 @@ APP_NAME = "Momentum Atlas"
|
|||||||
APP_TAGLINE = "A polished personal dashboard for tracking focus, energy, and small wins."
|
APP_TAGLINE = "A polished personal dashboard for tracking focus, energy, and small wins."
|
||||||
|
|
||||||
|
|
||||||
|
def _entries_for_request(request):
|
||||||
|
entries = MomentumEntry.objects.select_related("category")
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return entries.filter(user=request.user)
|
||||||
|
return entries.filter(user__isnull=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _categories_for_request(request):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
entry_filter = Q(entries__user=request.user)
|
||||||
|
else:
|
||||||
|
entry_filter = Q(entries__user__isnull=True)
|
||||||
|
return Category.objects.annotate(entry_total=Count("entries", filter=entry_filter))
|
||||||
|
|
||||||
|
|
||||||
def _build_weekly_trend(entries):
|
def _build_weekly_trend(entries):
|
||||||
today = timezone.localdate()
|
today = timezone.localdate()
|
||||||
start_date = today - timedelta(days=6)
|
start_date = today - timedelta(days=6)
|
||||||
@ -54,8 +71,8 @@ def _build_weekly_trend(entries):
|
|||||||
return trend
|
return trend
|
||||||
|
|
||||||
|
|
||||||
def _dashboard_context():
|
def _dashboard_context(request):
|
||||||
entries = MomentumEntry.objects.select_related("category")
|
entries = _entries_for_request(request)
|
||||||
recent_entries = entries[:6]
|
recent_entries = entries[:6]
|
||||||
last_30_days = timezone.localdate() - timedelta(days=29)
|
last_30_days = timezone.localdate() - timedelta(days=29)
|
||||||
stats_window = entries.filter(entry_date__gte=last_30_days)
|
stats_window = entries.filter(entry_date__gte=last_30_days)
|
||||||
@ -80,13 +97,16 @@ def _dashboard_context():
|
|||||||
spotlight = "Your recent focus trend is excellent—keep protecting that deep-work time."
|
spotlight = "Your recent focus trend is excellent—keep protecting that deep-work time."
|
||||||
elif focus_average >= 6:
|
elif focus_average >= 6:
|
||||||
spotlight = "Momentum is building. A little more consistency could turn this into a real streak."
|
spotlight = "Momentum is building. A little more consistency could turn this into a real streak."
|
||||||
else:
|
elif totals["total_entries"]:
|
||||||
spotlight = "A reset week might help—shrink the task list and aim for one clear win each day."
|
spotlight = "A reset week might help—shrink the task list and aim for one clear win each day."
|
||||||
|
else:
|
||||||
|
spotlight = "Your dashboard will start filling in as soon as you save the first check-in."
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"recent_entries": recent_entries,
|
"recent_entries": recent_entries,
|
||||||
"categories": Category.objects.all(),
|
"categories": _categories_for_request(request),
|
||||||
"weekly_trend": weekly_trend,
|
"weekly_trend": weekly_trend,
|
||||||
|
"is_demo_mode": not request.user.is_authenticated,
|
||||||
"stats": {
|
"stats": {
|
||||||
"total_entries": totals["total_entries"],
|
"total_entries": totals["total_entries"],
|
||||||
"avg_focus": focus_average,
|
"avg_focus": focus_average,
|
||||||
@ -105,10 +125,16 @@ def home(request):
|
|||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
messages.info(request, "Create a free account or log in to save personal check-ins.")
|
||||||
|
return redirect(f"{reverse('login')}?next={reverse('home')}")
|
||||||
|
|
||||||
form = MomentumEntryForm(request.POST)
|
form = MomentumEntryForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
entry = form.save()
|
entry = form.save(commit=False)
|
||||||
messages.success(request, "Momentum captured. Your new check-in is ready.")
|
entry.user = request.user
|
||||||
|
entry.save()
|
||||||
|
messages.success(request, "Momentum captured. Your new private check-in is ready.")
|
||||||
return redirect(f"{entry.get_absolute_url()}?created=1")
|
return redirect(f"{entry.get_absolute_url()}?created=1")
|
||||||
messages.error(request, "Please fix the form errors and try again.")
|
messages.error(request, "Please fix the form errors and try again.")
|
||||||
else:
|
else:
|
||||||
@ -126,14 +152,14 @@ def home(request):
|
|||||||
"page_title": f"{APP_NAME} | Daily focus dashboard",
|
"page_title": f"{APP_NAME} | Daily focus dashboard",
|
||||||
"meta_description": "Track daily focus, energy, and deep-work minutes in a polished Python dashboard.",
|
"meta_description": "Track daily focus, energy, and deep-work minutes in a polished Python dashboard.",
|
||||||
"form": form,
|
"form": form,
|
||||||
**_dashboard_context(),
|
**_dashboard_context(request),
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
|
||||||
def entry_list(request):
|
def entry_list(request):
|
||||||
selected_slug = request.GET.get("category", "")
|
selected_slug = request.GET.get("category", "")
|
||||||
entries = MomentumEntry.objects.select_related("category")
|
entries = _entries_for_request(request)
|
||||||
if selected_slug:
|
if selected_slug:
|
||||||
entries = entries.filter(category__slug=selected_slug)
|
entries = entries.filter(category__slug=selected_slug)
|
||||||
|
|
||||||
@ -141,24 +167,47 @@ def entry_list(request):
|
|||||||
"page_title": f"All check-ins | {APP_NAME}",
|
"page_title": f"All check-ins | {APP_NAME}",
|
||||||
"meta_description": "Browse recent check-ins and filter your momentum history by category.",
|
"meta_description": "Browse recent check-ins and filter your momentum history by category.",
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"categories": Category.objects.annotate(entry_total=Count("entries")),
|
"categories": _categories_for_request(request),
|
||||||
"selected_slug": selected_slug,
|
"selected_slug": selected_slug,
|
||||||
|
"is_demo_mode": not request.user.is_authenticated,
|
||||||
}
|
}
|
||||||
return render(request, "core/entry_list.html", context)
|
return render(request, "core/entry_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
def entry_detail(request, pk):
|
def entry_detail(request, pk):
|
||||||
entry = get_object_or_404(MomentumEntry.objects.select_related("category"), pk=pk)
|
scoped_entries = _entries_for_request(request)
|
||||||
related_entries = (
|
entry = get_object_or_404(scoped_entries, pk=pk)
|
||||||
MomentumEntry.objects.select_related("category")
|
related_entries = scoped_entries.filter(category=entry.category).exclude(pk=entry.pk)[:3]
|
||||||
.filter(category=entry.category)
|
|
||||||
.exclude(pk=entry.pk)[:3]
|
|
||||||
)
|
|
||||||
context = {
|
context = {
|
||||||
"page_title": f"{entry.title} | {APP_NAME}",
|
"page_title": f"{entry.title} | {APP_NAME}",
|
||||||
"meta_description": entry.takeaway,
|
"meta_description": entry.takeaway,
|
||||||
"entry": entry,
|
"entry": entry,
|
||||||
"related_entries": related_entries,
|
"related_entries": related_entries,
|
||||||
"created": request.GET.get("created") == "1",
|
"created": request.GET.get("created") == "1",
|
||||||
|
"is_demo_mode": not request.user.is_authenticated,
|
||||||
}
|
}
|
||||||
return render(request, "core/entry_detail.html", context)
|
return render(request, "core/entry_detail.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def signup(request):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect("home")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = SignUpForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
login(request, user)
|
||||||
|
messages.success(request, "Your account is ready. You can now save private momentum entries.")
|
||||||
|
return redirect(request.POST.get("next") or "home")
|
||||||
|
messages.error(request, "Please fix the sign-up form and try again.")
|
||||||
|
else:
|
||||||
|
form = SignUpForm()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"page_title": f"Create account | {APP_NAME}",
|
||||||
|
"meta_description": "Create a private Momentum Atlas account to save personal check-ins.",
|
||||||
|
"form": form,
|
||||||
|
"next_url": request.GET.get("next") or request.POST.get("next") or reverse("home"),
|
||||||
|
}
|
||||||
|
return render(request, "core/signup.html", context)
|
||||||
|
|||||||
@ -87,6 +87,27 @@ p {
|
|||||||
padding: 0.55rem 1rem !important;
|
padding: 0.55rem 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-user {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-form,
|
||||||
|
.inline-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-section {
|
.hero-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 1rem 0 4rem;
|
padding: 1rem 0 4rem;
|
||||||
@ -206,7 +227,8 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chip-row,
|
.chip-row,
|
||||||
.filter-row {
|
.filter-row,
|
||||||
|
.message-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@ -255,7 +277,8 @@ p {
|
|||||||
|
|
||||||
.section-heading h2,
|
.section-heading h2,
|
||||||
.subpage-header h1,
|
.subpage-header h1,
|
||||||
.detail-panel h1 {
|
.detail-panel h1,
|
||||||
|
.auth-card h1 {
|
||||||
font-size: clamp(2rem, 3vw, 3rem);
|
font-size: clamp(2rem, 3vw, 3rem);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
@ -269,12 +292,14 @@ p {
|
|||||||
.detail-panel,
|
.detail-panel,
|
||||||
.sidebar-panel,
|
.sidebar-panel,
|
||||||
.subpage-header,
|
.subpage-header,
|
||||||
.empty-state {
|
.empty-state,
|
||||||
|
.auth-card {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-panel .form-control,
|
.form-panel .form-control,
|
||||||
.form-panel .form-select {
|
.form-panel .form-select,
|
||||||
|
.auth-card .form-control {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
@ -345,6 +370,27 @@ p {
|
|||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-callout {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-shell {
|
||||||
|
padding: 1rem 0 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.trend-chart {
|
.trend-chart {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
@ -534,7 +580,12 @@ p {
|
|||||||
.sidebar-panel,
|
.sidebar-panel,
|
||||||
.subpage-header,
|
.subpage-header,
|
||||||
.empty-state,
|
.empty-state,
|
||||||
.insight-panel {
|
.insight-panel,
|
||||||
|
.auth-card {
|
||||||
padding: 1.4rem;
|
padding: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-user {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
static/images/favicon.svg
Normal file
17
static/images/favicon.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Momentum Atlas favicon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#0F766E"/>
|
||||||
|
<stop offset="1" stop-color="#F97316"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="glow" x1="12" y1="14" x2="52" y2="50" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffffff" stop-opacity="0.38"/>
|
||||||
|
<stop offset="1" stop-color="#ffffff" stop-opacity="0.08"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="18" fill="url(#bg)"/>
|
||||||
|
<rect x="8" y="8" width="48" height="48" rx="14" fill="url(#glow)"/>
|
||||||
|
<path d="M18 40 L27 31 L33 36 L46 22" fill="none" stroke="#FFFAF4" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
|
||||||
|
<circle cx="46" cy="22" r="4" fill="#F59E0B" stroke="#FFFAF4" stroke-width="2"/>
|
||||||
|
<path d="M18 46 H46" stroke="#FFFAF4" stroke-linecap="round" stroke-opacity="0.45" stroke-width="3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -87,6 +87,27 @@ p {
|
|||||||
padding: 0.55rem 1rem !important;
|
padding: 0.55rem 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-user {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-form,
|
||||||
|
.inline-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-section {
|
.hero-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 1rem 0 4rem;
|
padding: 1rem 0 4rem;
|
||||||
@ -206,7 +227,8 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chip-row,
|
.chip-row,
|
||||||
.filter-row {
|
.filter-row,
|
||||||
|
.message-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@ -255,7 +277,8 @@ p {
|
|||||||
|
|
||||||
.section-heading h2,
|
.section-heading h2,
|
||||||
.subpage-header h1,
|
.subpage-header h1,
|
||||||
.detail-panel h1 {
|
.detail-panel h1,
|
||||||
|
.auth-card h1 {
|
||||||
font-size: clamp(2rem, 3vw, 3rem);
|
font-size: clamp(2rem, 3vw, 3rem);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
@ -269,12 +292,14 @@ p {
|
|||||||
.detail-panel,
|
.detail-panel,
|
||||||
.sidebar-panel,
|
.sidebar-panel,
|
||||||
.subpage-header,
|
.subpage-header,
|
||||||
.empty-state {
|
.empty-state,
|
||||||
|
.auth-card {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-panel .form-control,
|
.form-panel .form-control,
|
||||||
.form-panel .form-select {
|
.form-panel .form-select,
|
||||||
|
.auth-card .form-control {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
@ -345,6 +370,27 @@ p {
|
|||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-callout {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-shell {
|
||||||
|
padding: 1rem 0 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.trend-chart {
|
.trend-chart {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
@ -534,7 +580,12 @@ p {
|
|||||||
.sidebar-panel,
|
.sidebar-panel,
|
||||||
.subpage-header,
|
.subpage-header,
|
||||||
.empty-state,
|
.empty-state,
|
||||||
.insight-panel {
|
.insight-panel,
|
||||||
|
.auth-card {
|
||||||
padding: 1.4rem;
|
padding: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-user {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
staticfiles/images/favicon.svg
Normal file
17
staticfiles/images/favicon.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Momentum Atlas favicon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#0F766E"/>
|
||||||
|
<stop offset="1" stop-color="#F97316"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="glow" x1="12" y1="14" x2="52" y2="50" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffffff" stop-opacity="0.38"/>
|
||||||
|
<stop offset="1" stop-color="#ffffff" stop-opacity="0.08"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="18" fill="url(#bg)"/>
|
||||||
|
<rect x="8" y="8" width="48" height="48" rx="14" fill="url(#glow)"/>
|
||||||
|
<path d="M18 40 L27 31 L33 36 L46 22" fill="none" stroke="#FFFAF4" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
|
||||||
|
<circle cx="46" cy="22" r="4" fill="#F59E0B" stroke="#FFFAF4" stroke-width="2"/>
|
||||||
|
<path d="M18 46 H46" stroke="#FFFAF4" stroke-linecap="round" stroke-opacity="0.45" stroke-width="3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
Loading…
x
Reference in New Issue
Block a user