Auto commit: 2026-04-16T11:25:44.037Z

This commit is contained in:
Flatlogic Bot 2026-04-16 11:25:44 +00:00
parent e0e36aaad0
commit 3a44f34cf9
24 changed files with 518 additions and 51 deletions

View File

@ -18,8 +18,10 @@ from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
from django.views.generic.base import RedirectView
urlpatterns = [
path("favicon.ico", RedirectView.as_view(url=settings.STATIC_URL + "images/favicon.svg", permanent=False)),
path("admin/", admin.site.urls),
path("", include("core.urls")),
]

View File

@ -14,12 +14,13 @@ class CategoryAdmin(admin.ModelAdmin):
class MomentumEntryAdmin(admin.ModelAdmin):
list_display = (
"title",
"user",
"entry_date",
"category",
"focus_score",
"energy_score",
"deep_work_minutes",
)
list_filter = ("category", "entry_date")
search_fields = ("title", "takeaway", "reflection")
list_filter = ("user", "category", "entry_date")
search_fields = ("title", "takeaway", "reflection", "user__username")
date_hierarchy = "entry_date"

View File

@ -1,4 +1,6 @@
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 .models import MomentumEntry
@ -64,3 +66,39 @@ class MomentumEntryForm(forms.ModelForm):
if len(takeaway.split()) < 3:
raise forms.ValidationError("Write a short sentence with at least three words.")
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

View 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),
),
]

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
@ -17,6 +18,13 @@ class Category(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")
title = models.CharField(max_length=120)
entry_date = models.DateField()

View File

@ -17,6 +17,7 @@
<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://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 }}">
{% block head %}{% endblock %}
</head>

View File

@ -6,6 +6,14 @@
{% block content %}
<div class="subpage-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 %}
{% if created %}
<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.
@ -52,8 +60,12 @@
<h2>Keep the loop going</h2>
</div>
<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-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>
</div>
<h3 class="h5">More in {{ entry.category.name }}</h3>

View File

@ -6,15 +6,32 @@
{% block content %}
<div class="subpage-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="subpage-header glass-panel mb-4">
<div>
<span class="eyebrow">History</span>
<h1>All momentum entries</h1>
<p>Filter by category and open any check-in for its confirmation/detail view.</p>
<h1>{% if is_demo_mode %}Demo momentum entries{% else %}Your momentum entries{% endif %}</h1>
<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 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>
{% if request.user.is_authenticated %}
<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>
@ -47,9 +64,13 @@
</div>
{% else %}
<div class="glass-panel empty-state">
<h2>No entries for this filter</h2>
<p>Try a different category or create a new check-in from the dashboard.</p>
<h2>{% if is_demo_mode %}No demo entries for this filter{% else %}No private entries for this filter{% endif %}</h2>
<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>
{% else %}
<a class="btn btn-accent mt-3" href="{% url 'signup' %}">Create your account</a>
{% endif %}
</div>
{% endif %}
</div>

View File

@ -14,9 +14,21 @@
</button>
<div class="collapse navbar-collapse" id="mainNav">
<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="{% 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>
</ul>
</div>
@ -29,12 +41,23 @@
<div class="container py-5 position-relative">
<div class="row align-items-center g-5">
<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>
<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">
{% if request.user.is_authenticated %}
<a class="btn btn-accent btn-lg" href="#check-in">Log todays 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 class="hero-meta d-flex flex-wrap gap-4 mt-4">
<div>
@ -49,7 +72,7 @@
</div>
<div class="col-lg-6">
<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="col-6">
<article class="metric-card">
@ -105,10 +128,17 @@
<div class="col-xl-7">
<div class="section-heading">
<span class="eyebrow">Workflow widget</span>
<h2>Capture one meaningful check-in.</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>
<h2>{% if request.user.is_authenticated %}Capture one meaningful check-in.{% else %}Create your account to start saving check-ins.{% endif %}</h2>
<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 class="glass-panel form-panel">
{% if request.user.is_authenticated %}
<form method="post" novalidate>
{% csrf_token %}
<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>
</div>
</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 class="col-xl-5">
@ -142,7 +185,7 @@
<div class="stack-grid">
<article class="glass-panel feature-card">
<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 class="glass-panel feature-card">
<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="section-heading compact mb-0">
<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>
<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 class="glass-panel chart-panel">
<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="section-heading compact mb-0">
<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>
<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>
{% if recent_entries %}
<div class="row g-4">
@ -216,8 +259,14 @@
</div>
{% else %}
<div class="glass-panel empty-state">
<h3>No entries yet</h3>
<p>Start with one check-in above and this dashboard will instantly become your first useful Python product demo.</p>
<h3>{% if request.user.is_authenticated %}No private entries yet{% else %}No demo entries yet{% endif %}</h3>
<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>
{% endif %}
</div>

View 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 %}

View 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 %}

View File

@ -1,9 +1,22 @@
from django.contrib.auth import views as auth_views
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 = [
path("", home, name="home"),
path("entries/", entry_list, name="entry_list"),
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"),
]

View File

@ -4,12 +4,14 @@ from datetime import timedelta
from django import get_version as django_version
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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from .forms import MomentumEntryForm
from .forms import MomentumEntryForm, SignUpForm
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."
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):
today = timezone.localdate()
start_date = today - timedelta(days=6)
@ -54,8 +71,8 @@ def _build_weekly_trend(entries):
return trend
def _dashboard_context():
entries = MomentumEntry.objects.select_related("category")
def _dashboard_context(request):
entries = _entries_for_request(request)
recent_entries = entries[:6]
last_30_days = timezone.localdate() - timedelta(days=29)
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."
elif focus_average >= 6:
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."
else:
spotlight = "Your dashboard will start filling in as soon as you save the first check-in."
return {
"recent_entries": recent_entries,
"categories": Category.objects.all(),
"categories": _categories_for_request(request),
"weekly_trend": weekly_trend,
"is_demo_mode": not request.user.is_authenticated,
"stats": {
"total_entries": totals["total_entries"],
"avg_focus": focus_average,
@ -105,10 +125,16 @@ def home(request):
now = timezone.now()
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)
if form.is_valid():
entry = form.save()
messages.success(request, "Momentum captured. Your new check-in is ready.")
entry = form.save(commit=False)
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")
messages.error(request, "Please fix the form errors and try again.")
else:
@ -126,14 +152,14 @@ def home(request):
"page_title": f"{APP_NAME} | Daily focus dashboard",
"meta_description": "Track daily focus, energy, and deep-work minutes in a polished Python dashboard.",
"form": form,
**_dashboard_context(),
**_dashboard_context(request),
}
return render(request, "core/index.html", context)
def entry_list(request):
selected_slug = request.GET.get("category", "")
entries = MomentumEntry.objects.select_related("category")
entries = _entries_for_request(request)
if 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}",
"meta_description": "Browse recent check-ins and filter your momentum history by category.",
"entries": entries,
"categories": Category.objects.annotate(entry_total=Count("entries")),
"categories": _categories_for_request(request),
"selected_slug": selected_slug,
"is_demo_mode": not request.user.is_authenticated,
}
return render(request, "core/entry_list.html", context)
def entry_detail(request, pk):
entry = get_object_or_404(MomentumEntry.objects.select_related("category"), pk=pk)
related_entries = (
MomentumEntry.objects.select_related("category")
.filter(category=entry.category)
.exclude(pk=entry.pk)[:3]
)
scoped_entries = _entries_for_request(request)
entry = get_object_or_404(scoped_entries, pk=pk)
related_entries = scoped_entries.filter(category=entry.category).exclude(pk=entry.pk)[:3]
context = {
"page_title": f"{entry.title} | {APP_NAME}",
"meta_description": entry.takeaway,
"entry": entry,
"related_entries": related_entries,
"created": request.GET.get("created") == "1",
"is_demo_mode": not request.user.is_authenticated,
}
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)

View File

@ -87,6 +87,27 @@ p {
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 {
position: relative;
padding: 1rem 0 4rem;
@ -206,7 +227,8 @@ p {
}
.chip-row,
.filter-row {
.filter-row,
.message-stack {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
@ -255,7 +277,8 @@ p {
.section-heading h2,
.subpage-header h1,
.detail-panel h1 {
.detail-panel h1,
.auth-card h1 {
font-size: clamp(2rem, 3vw, 3rem);
margin-bottom: 0.8rem;
}
@ -269,12 +292,14 @@ p {
.detail-panel,
.sidebar-panel,
.subpage-header,
.empty-state {
.empty-state,
.auth-card {
padding: 2rem;
}
.form-panel .form-control,
.form-panel .form-select {
.form-panel .form-select,
.auth-card .form-control {
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.35);
padding: 0.9rem 1rem;
@ -345,6 +370,27 @@ p {
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 {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
@ -534,7 +580,12 @@ p {
.sidebar-panel,
.subpage-header,
.empty-state,
.insight-panel {
.insight-panel,
.auth-card {
padding: 1.4rem;
}
.nav-user {
width: fit-content;
}
}

17
static/images/favicon.svg Normal file
View 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

View File

@ -87,6 +87,27 @@ p {
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 {
position: relative;
padding: 1rem 0 4rem;
@ -206,7 +227,8 @@ p {
}
.chip-row,
.filter-row {
.filter-row,
.message-stack {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
@ -255,7 +277,8 @@ p {
.section-heading h2,
.subpage-header h1,
.detail-panel h1 {
.detail-panel h1,
.auth-card h1 {
font-size: clamp(2rem, 3vw, 3rem);
margin-bottom: 0.8rem;
}
@ -269,12 +292,14 @@ p {
.detail-panel,
.sidebar-panel,
.subpage-header,
.empty-state {
.empty-state,
.auth-card {
padding: 2rem;
}
.form-panel .form-control,
.form-panel .form-select {
.form-panel .form-select,
.auth-card .form-control {
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.35);
padding: 0.9rem 1rem;
@ -345,6 +370,27 @@ p {
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 {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
@ -534,7 +580,12 @@ p {
.sidebar-panel,
.subpage-header,
.empty-state,
.insight-panel {
.insight-panel,
.auth-card {
padding: 1.4rem;
}
.nav-user {
width: fit-content;
}
}

View 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