Compare commits

..

6 Commits

Author SHA1 Message Date
Flatlogic Bot
7c96dcba34 Revert to version 9b2308a 2026-04-16 13:48:51 +00:00
Flatlogic Bot
1631c3ee60 h w 2026-04-16 13:48:45 +00:00
Flatlogic Bot
9b2308aeac Auto commit: 2026-04-16T13:25:51.679Z 2026-04-16 13:25:51 +00:00
Flatlogic Bot
3a44f34cf9 Auto commit: 2026-04-16T11:25:44.037Z 2026-04-16 11:25:44 +00:00
Flatlogic Bot
e0e36aaad0 Auto commit: 2026-04-16T11:16:12.257Z 2026-04-16 11:16:12 +00:00
Flatlogic Bot
28d370c224 Auto commit: 2026-04-16T11:11:36.691Z 2026-04-16 11:11:36 +00:00
32 changed files with 3106 additions and 172 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -1,3 +1,26 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. from .models import Category, MomentumEntry
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ("name", "slug", "accent_color")
prepopulated_fields = {"slug": ("name",)}
search_fields = ("name", "description")
@admin.register(MomentumEntry)
class MomentumEntryAdmin(admin.ModelAdmin):
list_display = (
"title",
"user",
"entry_date",
"category",
"focus_score",
"energy_score",
"deep_work_minutes",
)
list_filter = ("user", "category", "entry_date")
search_fields = ("title", "takeaway", "reflection", "user__username")
date_hierarchy = "entry_date"

104
core/forms.py Normal file
View File

@ -0,0 +1,104 @@
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
class MomentumEntryForm(forms.ModelForm):
class Meta:
model = MomentumEntry
fields = [
"title",
"category",
"entry_date",
"focus_score",
"energy_score",
"deep_work_minutes",
"takeaway",
"reflection",
]
widgets = {
"title": forms.TextInput(
attrs={"placeholder": "Shipped a habit, finished a lesson, or stayed focused"}
),
"entry_date": forms.DateInput(attrs={"type": "date"}),
"focus_score": forms.NumberInput(attrs={"min": 1, "max": 10}),
"energy_score": forms.NumberInput(attrs={"min": 1, "max": 10}),
"deep_work_minutes": forms.NumberInput(attrs={"min": 0, "max": 960, "step": 5}),
"takeaway": forms.TextInput(
attrs={"placeholder": "One sentence that captures todays momentum"}
),
"reflection": forms.Textarea(
attrs={"rows": 4, "placeholder": "What worked, what felt hard, what should tomorrow look like?"}
),
}
labels = {
"title": "What did you move forward?",
"category": "Category",
"entry_date": "Date",
"focus_score": "Focus score",
"energy_score": "Energy score",
"deep_work_minutes": "Deep-work minutes",
"takeaway": "Main takeaway",
"reflection": "Reflection",
}
help_texts = {
"focus_score": "1 = distracted, 10 = locked in.",
"energy_score": "1 = drained, 10 = fully charged.",
"deep_work_minutes": "Minutes spent on meaningful work today.",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["entry_date"].initial = timezone.localdate()
for name, field in self.fields.items():
css_class = "form-select" if isinstance(field.widget, forms.Select) else "form-control"
if name in {"focus_score", "energy_score", "deep_work_minutes", "entry_date"}:
css_class = "form-control"
field.widget.attrs["class"] = f"{field.widget.attrs.get('class', '')} {css_class}".strip()
field.widget.attrs.setdefault("autocomplete", "off")
self.fields["reflection"].required = False
def clean_takeaway(self):
takeaway = self.cleaned_data["takeaway"].strip()
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,47 @@
# Generated by Django 5.2.7 on 2026-04-16 11:08
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80, unique=True)),
('slug', models.SlugField(max_length=80, unique=True)),
('description', models.CharField(max_length=160)),
('accent_color', models.CharField(default='#0F766E', max_length=7)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='MomentumEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=120)),
('entry_date', models.DateField()),
('focus_score', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
('energy_score', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
('deep_work_minutes', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(960)])),
('takeaway', models.CharField(max_length=160)),
('reflection', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='core.category')),
],
options={
'ordering': ['-entry_date', '-created_at'],
},
),
]

View File

@ -0,0 +1,115 @@
from datetime import date
from django.db import migrations
CATEGORIES = [
{
"name": "Learning",
"slug": "learning",
"description": "Study sessions, experiments, and small breakthroughs.",
"accent_color": "#0F766E",
},
{
"name": "Deep Work",
"slug": "deep-work",
"description": "Quiet execution blocks that move the hard work forward.",
"accent_color": "#F97316",
},
{
"name": "Wellbeing",
"slug": "wellbeing",
"description": "Energy, routines, and recovery habits that support momentum.",
"accent_color": "#F59E0B",
},
]
def seed_demo_data(apps, schema_editor):
Category = apps.get_model("core", "Category")
MomentumEntry = apps.get_model("core", "MomentumEntry")
category_map = {}
for item in CATEGORIES:
category, _ = Category.objects.get_or_create(
slug=item["slug"],
defaults={
"name": item["name"],
"description": item["description"],
"accent_color": item["accent_color"],
},
)
category_map[item["slug"]] = category
if MomentumEntry.objects.exists():
return
MomentumEntry.objects.bulk_create(
[
MomentumEntry(
category=category_map["learning"],
title="Finished a Python walkthrough",
entry_date=date(2026, 4, 10),
focus_score=8,
energy_score=7,
deep_work_minutes=95,
takeaway="Turned a tutorial into notes I can actually reuse later.",
reflection="The best part was rewriting the idea in my own words instead of copying line by line.",
),
MomentumEntry(
category=category_map["deep-work"],
title="Protected a no-notification build block",
entry_date=date(2026, 4, 12),
focus_score=9,
energy_score=8,
deep_work_minutes=140,
takeaway="A single distraction-free block created more progress than a scattered full day.",
reflection="Turning off notifications before starting made the session feel calm and fast.",
),
MomentumEntry(
category=category_map["wellbeing"],
title="Reset the afternoon slump",
entry_date=date(2026, 4, 14),
focus_score=6,
energy_score=8,
deep_work_minutes=60,
takeaway="A walk and lighter task list rescued the day instead of writing it off.",
reflection="I should use recovery intentionally, not only when things already feel off.",
),
MomentumEntry(
category=category_map["learning"],
title="Built the first Django tracker flow",
entry_date=date(2026, 4, 15),
focus_score=8,
energy_score=8,
deep_work_minutes=125,
takeaway="Shipping a tiny end-to-end slice feels more motivating than polishing ideas in isolation.",
reflection="Seeing create, list, and detail views together makes the project feel real.",
),
]
)
def reverse_seed_demo_data(apps, schema_editor):
Category = apps.get_model("core", "Category")
MomentumEntry = apps.get_model("core", "MomentumEntry")
MomentumEntry.objects.filter(
title__in=[
"Finished a Python walkthrough",
"Protected a no-notification build block",
"Reset the afternoon slump",
"Built the first Django tracker flow",
]
).delete()
Category.objects.filter(slug__in=["learning", "deep-work", "wellbeing"]).delete()
class Migration(migrations.Migration):
dependencies = [
("core", "0001_initial"),
]
operations = [
migrations.RunPython(seed_demo_data, reverse_seed_demo_data),
]

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,55 @@
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=80, unique=True)
slug = models.SlugField(max_length=80, unique=True)
description = models.CharField(max_length=160)
accent_color = models.CharField(max_length=7, default="#0F766E")
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
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()
focus_score = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10)]
)
energy_score = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10)]
)
deep_work_minutes = models.PositiveIntegerField(
validators=[MinValueValidator(0), MaxValueValidator(960)]
)
takeaway = models.CharField(max_length=160)
reflection = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-entry_date", "-created_at"]
def __str__(self):
return f"{self.title} ({self.entry_date:%Y-%m-%d})"
@property
def momentum_score(self):
return round((self.focus_score + self.energy_score) / 2, 1)
def get_absolute_url(self):
return reverse("entry_detail", args=[self.pk])

View File

@ -1,25 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
{% load static %}
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1">
{% if project_description %} <title>{% block title %}{{ page_title|default:"Momentum Atlas" }}{% endblock %}</title>
<meta name="description" content="{{ project_description }}"> <meta name="description" content="{% block meta_description %}{{ meta_description|default:project_description }}{% endblock %}">
<meta property="og:description" content="{{ project_description }}"> <meta name="keywords" content="productivity dashboard, personal tracker, python django app">
<meta property="twitter:description" content="{{ project_description }}"> <meta name="author" content="Flatlogic">
{% endif %}
{% if project_image_url %} {% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}"> <meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}"> <meta property="twitter:image" content="{{ project_image_url }}">
{% endif %} {% endif %}
{% load static %} <link rel="preconnect" href="https://fonts.googleapis.com">
<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 }}"> <link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
{% block content %}{% endblock %} {% block content %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block meta_description %}{{ meta_description }}{% endblock %}
{% 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.
</div>
{% endif %}
<div class="row g-4">
<div class="col-lg-8">
<article class="glass-panel detail-panel h-100">
<div class="entry-card-top mb-3">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"F j, Y" }}</span>
</div>
<h1>{{ entry.title }}</h1>
<p class="detail-lead">{{ entry.takeaway }}</p>
<div class="detail-metrics">
<div class="metric-card detail-metric">
<span>Focus</span>
<strong>{{ entry.focus_score }}/10</strong>
</div>
<div class="metric-card detail-metric">
<span>Energy</span>
<strong>{{ entry.energy_score }}/10</strong>
</div>
<div class="metric-card detail-metric">
<span>Deep work</span>
<strong>{{ entry.deep_work_minutes }}m</strong>
</div>
<div class="metric-card detail-metric">
<span>Momentum</span>
<strong>{{ entry.momentum_score }}/10</strong>
</div>
</div>
<section class="reflection-block">
<h2>Reflection</h2>
<p>{{ entry.reflection|default:"No extra reflection was added for this day."|linebreaksbr }}</p>
</section>
</article>
</div>
<div class="col-lg-4">
<aside class="glass-panel sidebar-panel h-100">
<div class="section-heading compact mb-3">
<span class="eyebrow">Next actions</span>
<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>
{% 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>
<div class="related-list">
{% for related in related_entries %}
<a class="related-item" href="{% url 'entry_detail' related.pk %}">
<strong>{{ related.title }}</strong>
<span>{{ related.entry_date|date:"M j" }} · {{ related.takeaway }}</span>
</a>
{% empty %}
<p class="mb-0 text-muted">This is the first entry in this category.</p>
{% endfor %}
</div>
</aside>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,191 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block meta_description %}{{ meta_description }}{% endblock %}
{% 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>{% 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, spot patterns faster, and open any entry for the full detail view.{% endif %}</p>
</div>
<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>
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Total check-ins</span>
<strong>{{ history_overview.total_entries }}</strong>
<p>{% if history_overview.latest_entry %}Latest: {{ history_overview.latest_entry.entry_date|date:"M j, Y" }}{% else %}No entries yet{% endif %}</p>
</article>
</div>
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Avg. momentum</span>
<strong>{{ history_overview.avg_momentum }}/10</strong>
<p>Focus {{ history_overview.avg_focus }}/10 · Energy {{ history_overview.avg_energy }}/10</p>
</article>
</div>
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Current streak</span>
<strong>{{ history_overview.streak }} day{{ history_overview.streak|pluralize }}</strong>
<p>Top lane: {{ history_overview.top_category }}</p>
</article>
</div>
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Deep work total</span>
<strong>{{ history_overview.total_minutes }}m</strong>
<p>{% if is_demo_mode %}Sample time invested{% else %}Time protected for focused work{% endif %}</p>
</article>
</div>
</div>
<div class="filter-row mb-4">
<a class="filter-chip {% if not selected_slug %}active{% endif %}" href="{% url 'entry_list' %}">All categories</a>
{% for category in categories %}
<a class="filter-chip {% if selected_slug == category.slug %}active{% endif %}" href="{% url 'entry_list' %}?category={{ category.slug }}">{{ category.name }} · {{ category.entry_total }}</a>
{% endfor %}
</div>
{% if entries %}
<div class="row g-4 mb-4">
<div class="col-xl-7">
<section class="glass-panel chart-panel h-100">
<div class="chart-panel-top">
<div>
<span class="panel-label">Recent pattern</span>
<h2 class="h3 mb-2">Last {{ recent_activity|length }} check-ins at a glance</h2>
<p class="mb-0">Quick comparison bars make it easier to notice whether focus, energy, or deep work is drifting.</p>
</div>
</div>
<div class="activity-list">
{% for item in recent_activity %}
<article class="activity-row">
<div class="activity-row-head">
<div>
<span class="category-badge">{{ item.entry.category.name }}</span>
<h3><a href="{% url 'entry_detail' item.entry.pk %}">{{ item.entry.title }}</a></h3>
</div>
<span class="entry-date">{{ item.entry.entry_date|date:"M j" }}</span>
</div>
<div class="score-track-group">
<div class="score-track-item">
<span>Focus</span>
<div class="score-track"><div class="score-fill focus" style="width: {{ item.focus_width }}%"></div></div>
<strong>{{ item.entry.focus_score }}/10</strong>
</div>
<div class="score-track-item">
<span>Energy</span>
<div class="score-track"><div class="score-fill energy" style="width: {{ item.energy_width }}%"></div></div>
<strong>{{ item.entry.energy_score }}/10</strong>
</div>
<div class="score-track-item minutes">
<span>Deep work</span>
<div class="score-track"><div class="score-fill minutes" style="width: {{ item.minutes_width }}%"></div></div>
<strong>{{ item.entry.deep_work_minutes }}m</strong>
</div>
</div>
</article>
{% endfor %}
</div>
</section>
</div>
<div class="col-xl-5">
<aside class="glass-panel chart-panel h-100">
<div class="chart-panel-top">
<div>
<span class="panel-label">Category lanes</span>
<h2 class="h3 mb-2">Where your momentum shows up most</h2>
<p class="mb-0">These lanes rank the strongest categories in the current filtered history.</p>
</div>
</div>
<div class="lane-list">
{% for lane in category_breakdown %}
<article class="lane-card">
<div class="lane-card-top">
<div>
<strong>{{ lane.name }}</strong>
<span>{{ lane.entry_total }} entry{{ lane.entry_total|pluralize }}</span>
</div>
<span>{{ lane.avg_momentum }}/10 avg</span>
</div>
<div class="lane-track" aria-hidden="true">
<div class="lane-fill" style="width: {{ lane.share_percent }}%; background: linear-gradient(135deg, {{ lane.accent_color }}, var(--brand-highlight));"></div>
</div>
<div class="lane-meta">
<span>{{ lane.total_minutes }} minutes tracked</span>
<a href="{% url 'entry_list' %}?category={{ lane.slug }}">Open lane</a>
</div>
</article>
{% endfor %}
</div>
</aside>
</div>
</div>
<div class="row g-4">
{% for entry in entries %}
<div class="col-lg-4 col-md-6">
<article class="glass-panel entry-card entry-card-enhanced h-100" style="--entry-accent: {{ entry.category.accent_color }};">
<div class="entry-card-top">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"M j, Y" }}</span>
</div>
<h2 class="h4"><a href="{% url 'entry_detail' entry.pk %}">{{ entry.title }}</a></h2>
<p>{{ entry.takeaway }}</p>
<div class="entry-stats">
<span>Focus {{ entry.focus_score }}/10</span>
<span>Energy {{ entry.energy_score }}/10</span>
<span>{{ entry.deep_work_minutes }} min</span>
</div>
<div class="entry-card-footer">
<div class="mini-meter">
<span>Momentum score</span>
<strong>{{ entry.momentum_score }}/10</strong>
</div>
<a class="text-link" href="{% url 'entry_detail' entry.pk %}">View details</a>
</div>
</article>
</div>
{% endfor %}
</div>
{% else %}
<div class="glass-panel empty-state">
<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>
</div>
{% endblock %}

View File

@ -1,145 +1,307 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %} {% block title %}{{ page_title }}{% endblock %}
{% block meta_description %}{{ meta_description }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<main> <div class="site-shell">
<div class="card"> <header class="site-header">
<h1>Analyzing your requirements and generating your app…</h1> <nav class="navbar navbar-expand-lg navbar-dark main-nav">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <div class="container py-3">
<span class="sr-only">Loading…</span> <a class="navbar-brand brand-mark" href="{% url 'home' %}">Momentum Atlas</a>
<button class="navbar-toggler border-0 shadow-none" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</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">{% 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' %}">{% 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> </div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p> </div>
<p class="hint">This page will refresh automatically as the plan is implemented.</p> </nav>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> <section class="hero-section">
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code> <div class="hero-orb hero-orb-one"></div>
<div class="hero-orb hero-orb-two"></div>
<div class="container py-5 position-relative">
<div class="row align-items-center g-5">
<div class="col-lg-6">
<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">
{% 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 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>
<span class="hero-meta-label">Runtime</span>
<strong>Django {{ django_version }} · Python {{ python_version }}</strong>
</div>
<div>
<span class="hero-meta-label">Updated</span>
<strong>{{ current_time|date:"M j, Y · H:i" }} UTC</strong>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="glass-panel insight-panel">
<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">
<span>Total check-ins</span>
<strong>{{ stats.total_entries }}</strong>
</article>
</div>
<div class="col-6">
<article class="metric-card">
<span>Active days</span>
<strong>{{ stats.active_days }}</strong>
</article>
</div>
<div class="col-6">
<article class="metric-card">
<span>Avg. focus</span>
<strong>{{ stats.avg_focus }}/10</strong>
</article>
</div>
<div class="col-6">
<article class="metric-card">
<span>Deep work</span>
<strong>{{ stats.total_minutes }}m</strong>
</article>
</div>
</div>
<div class="spotlight-note mt-4">
<span class="panel-label">Momentum spotlight</span>
<p>{{ stats.spotlight }}</p>
<div class="chip-row">
<span class="trend-chip">Top lane: {{ stats.top_category }}</span>
<span class="trend-chip">Avg. energy: {{ stats.avg_energy }}/10</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</header>
<main>
<section class="section-block" id="check-in">
<div class="container">
{% 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 g-4 align-items-start">
<div class="col-xl-7">
<div class="section-heading">
<span class="eyebrow">Workflow widget</span>
<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> </p>
</div> </div>
</main> <div class="glass-panel form-panel">
<footer> {% if request.user.is_authenticated %}
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) <form method="post" novalidate>
</footer> {% csrf_token %}
<div class="row g-3">
{% for field in form %}
<div class="col-12 {% if field.name == 'entry_date' or field.name == 'focus_score' or field.name == 'energy_score' or field.name == 'deep_work_minutes' %}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>
<div class="d-flex flex-wrap gap-3 align-items-center mt-4">
<button type="submit" class="btn btn-accent btn-lg">Save check-in</button>
<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">
<div class="section-heading compact">
<span class="eyebrow">What this MVP already does</span>
<h2>Thin slice, real workflow.</h2>
</div>
<div class="stack-grid">
<article class="glass-panel feature-card">
<h3>Create</h3>
<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>
<p>Each new entry opens its own detail page with a success state and related history.</p>
</article>
<article class="glass-panel feature-card">
<h3>Review</h3>
<p>Use weekly bars and the history page to notice patterns instead of collecting dead data.</p>
</article>
</div>
</div>
</div>
</div>
</section>
<section class="section-block section-muted" id="weekly-trend">
<div class="container">
<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>{% 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' %}">{% if is_demo_mode %}Open demo history{% else %}Open your history{% endif %}</a>
</div>
<div class="glass-panel chart-panel">
<div class="chart-panel-top chart-panel-top-inline">
<div>
<span class="panel-label">Compare your week</span>
<p class="chart-intro mb-0">Two bars per day show whether focus and energy are moving together or drifting apart.</p>
</div>
<div class="chart-legend" aria-label="Chart legend">
<span class="legend-pill"><span class="legend-dot focus"></span>Focus</span>
<span class="legend-pill"><span class="legend-dot energy"></span>Energy</span>
</div>
</div>
<div class="trend-chart">
{% for day in weekly_trend %}
<article class="trend-day">
<div class="trend-bars">
<div class="trend-bar focus level-{{ day.focus_level }}" aria-label="Focus {{ day.focus }} out of 10"></div>
<div class="trend-bar energy level-{{ day.energy_level }}" aria-label="Energy {{ day.energy }} out of 10"></div>
</div>
<div class="trend-values">{{ day.focus }}/{{ day.energy }}</div>
<strong>{{ day.label }}</strong>
<span>{{ day.minutes }}m</span>
</article>
{% endfor %}
</div>
<div class="trend-summary-grid">
<article class="trend-summary-card">
<span>Check-in days</span>
<strong>{{ weekly_summary.check_in_days }}/7</strong>
</article>
<article class="trend-summary-card">
<span>Deep work logged</span>
<strong>{{ weekly_summary.total_minutes }}m</strong>
</article>
<article class="trend-summary-card">
<span>Strongest day</span>
<strong>{{ weekly_summary.strongest_label }} · {{ weekly_summary.strongest_score }}/10</strong>
</article>
</div>
</div>
</div>
</section>
<section class="section-block">
<div class="container">
<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>{% if is_demo_mode %}Demo momentum history preview{% else %}Your momentum history preview{% endif %}</h2>
</div>
<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">
{% for entry in recent_entries %}
<div class="col-lg-4 col-md-6">
<article class="glass-panel entry-card entry-card-enhanced h-100" style="--entry-accent: {{ entry.category.accent_color }};">
<div class="entry-card-top">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"M j" }}</span>
</div>
<h3><a href="{% url 'entry_detail' entry.pk %}">{{ entry.title }}</a></h3>
<p>{{ entry.takeaway }}</p>
<div class="entry-stats">
<span>Focus {{ entry.focus_score }}/10</span>
<span>Energy {{ entry.energy_score }}/10</span>
<span>{{ entry.deep_work_minutes }} min</span>
</div>
<div class="entry-card-footer">
<div class="mini-meter">
<span>Momentum score</span>
<strong>{{ entry.momentum_score }}/10</strong>
</div>
<a class="text-link" href="{% url 'entry_detail' entry.pk %}">View details</a>
</div>
</article>
</div>
{% endfor %}
</div>
{% else %}
<div class="glass-panel empty-state">
<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>
</section>
</main>
</div>
{% endblock %} {% endblock %}

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,3 +1,37 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
# Create your tests here. from .models import Category, MomentumEntry
class MomentumViewsTests(TestCase):
def setUp(self):
self.category = Category.objects.create(
name="Learning",
slug="learning",
description="Tracking study sessions",
accent_color="#0F766E",
)
def test_home_page_loads(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Momentum Atlas")
def test_post_creates_entry_and_redirects(self):
response = self.client.post(
reverse("home"),
{
"title": "Finished a Python kata",
"category": self.category.pk,
"entry_date": timezone.localdate().isoformat(),
"focus_score": 8,
"energy_score": 7,
"deep_work_minutes": 90,
"takeaway": "Made steady progress with functions today.",
"reflection": "Felt strong after removing distractions.",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(MomentumEntry.objects.count(), 1)

View File

@ -1,7 +1,22 @@
from django.contrib.auth import views as auth_views
from django.urls import path from django.urls import path
from .views import 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/<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

@ -1,25 +1,344 @@
import os import os
import platform import platform
from datetime import timedelta
from django import get_version as django_version from django import get_version as django_version
from django.shortcuts import render from django.contrib import messages
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 django.utils import timezone
from .forms import MomentumEntryForm, SignUpForm
from .models import Category, MomentumEntry
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)
trend_source = (
entries.filter(entry_date__gte=start_date, entry_date__lte=today)
.values("entry_date")
.annotate(
avg_focus=Coalesce(Avg("focus_score"), 0.0),
avg_energy=Coalesce(Avg("energy_score"), 0.0),
total_minutes=Coalesce(Sum("deep_work_minutes"), 0),
)
)
by_date = {row["entry_date"]: row for row in trend_source}
trend = []
for offset in range(7):
day = start_date + timedelta(days=offset)
row = by_date.get(day, {})
focus = float(row.get("avg_focus") or 0)
energy = float(row.get("avg_energy") or 0)
minutes = int(row.get("total_minutes") or 0)
focus_level = int(round((focus / 10) * 10) * 10) if focus else 0
energy_level = int(round((energy / 10) * 10) * 10) if energy else 0
trend.append(
{
"date": day,
"label": day.strftime("%a"),
"focus": round(focus, 1),
"energy": round(energy, 1),
"minutes": minutes,
"focus_level": max(0, min(100, focus_level)),
"energy_level": max(0, min(100, energy_level)),
}
)
return trend
def _build_weekly_summary(weekly_trend):
check_in_days = sum(1 for day in weekly_trend if day["focus"] or day["energy"] or day["minutes"])
total_minutes = sum(day["minutes"] for day in weekly_trend)
strongest_day = max(
weekly_trend,
key=lambda day: (day["focus"] + day["energy"], day["minutes"]),
default=None,
)
strongest_has_data = bool(
strongest_day and (strongest_day["focus"] or strongest_day["energy"] or strongest_day["minutes"])
)
strongest_score = round(((strongest_day["focus"] + strongest_day["energy"]) / 2), 1) if strongest_has_data else 0
return {
"check_in_days": check_in_days,
"total_minutes": total_minutes,
"strongest_label": strongest_day["label"] if strongest_has_data else "No data yet",
"strongest_score": strongest_score,
}
def _build_history_overview(entries):
totals = entries.aggregate(
total_entries=Count("id"),
avg_focus=Coalesce(Avg("focus_score"), 0.0),
avg_energy=Coalesce(Avg("energy_score"), 0.0),
total_minutes=Coalesce(Sum("deep_work_minutes"), 0),
)
avg_focus = float(totals["avg_focus"] or 0)
avg_energy = float(totals["avg_energy"] or 0)
avg_momentum = round((avg_focus + avg_energy) / 2, 1) if totals["total_entries"] else 0
ordered_dates = []
seen_dates = set()
for entry_date in entries.values_list("entry_date", flat=True):
if entry_date not in seen_dates:
ordered_dates.append(entry_date)
seen_dates.add(entry_date)
streak = 0
previous_date = None
for entry_date in ordered_dates:
if previous_date is None:
streak = 1
previous_date = entry_date
continue
if previous_date - timedelta(days=1) == entry_date:
streak += 1
previous_date = entry_date
continue
break
top_category = (
entries.values("category__name")
.annotate(total=Count("id"))
.order_by("-total", "category__name")
.first()
)
latest_entry = entries.first()
return {
"total_entries": totals["total_entries"],
"avg_focus": round(avg_focus, 1),
"avg_energy": round(avg_energy, 1),
"avg_momentum": avg_momentum,
"total_minutes": int(totals["total_minutes"] or 0),
"streak": streak if totals["total_entries"] else 0,
"latest_entry": latest_entry,
"top_category": top_category["category__name"] if top_category else "No category yet",
}
def _build_recent_activity(entries, limit=7):
recent_entries = list(entries[:limit])
if not recent_entries:
return []
recent_entries.reverse()
max_minutes = max((entry.deep_work_minutes for entry in recent_entries), default=0)
activity = []
for entry in recent_entries:
momentum = float(entry.momentum_score)
minutes_width = 0
if entry.deep_work_minutes and max_minutes:
minutes_width = max(14, int(round((entry.deep_work_minutes / max_minutes) * 100)))
activity.append(
{
"entry": entry,
"focus_width": entry.focus_score * 10,
"energy_width": entry.energy_score * 10,
"momentum_width": int(round(momentum * 10)),
"minutes_width": minutes_width,
}
)
return activity
def _build_category_breakdown(entries):
grouped = list(
entries.values("category__name", "category__slug", "category__accent_color")
.annotate(
entry_total=Count("id"),
avg_focus=Coalesce(Avg("focus_score"), 0.0),
avg_energy=Coalesce(Avg("energy_score"), 0.0),
total_minutes=Coalesce(Sum("deep_work_minutes"), 0),
)
.order_by("-entry_total", "category__name")[:4]
)
total_entries = sum(item["entry_total"] for item in grouped) or 1
breakdown = []
for item in grouped:
avg_momentum = round((float(item["avg_focus"] or 0) + float(item["avg_energy"] or 0)) / 2, 1)
breakdown.append(
{
"name": item["category__name"],
"slug": item["category__slug"],
"accent_color": item["category__accent_color"] or "#0F766E",
"entry_total": item["entry_total"],
"total_minutes": int(item["total_minutes"] or 0),
"avg_momentum": avg_momentum,
"share_percent": max(8, int(round((item["entry_total"] / total_entries) * 100))),
"momentum_width": max(8, int(round(avg_momentum * 10))) if avg_momentum else 0,
}
)
return breakdown
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)
totals = stats_window.aggregate(
total_entries=Count("id"),
avg_focus=Coalesce(Avg("focus_score"), 0.0),
avg_energy=Coalesce(Avg("energy_score"), 0.0),
total_minutes=Coalesce(Sum("deep_work_minutes"), 0),
)
active_days = stats_window.values("entry_date").distinct().count()
top_category = (
stats_window.values("category__name")
.annotate(total=Count("id"))
.order_by("-total", "category__name")
.first()
)
weekly_trend = _build_weekly_trend(entries)
focus_average = round(float(totals["avg_focus"] or 0), 1)
energy_average = round(float(totals["avg_energy"] or 0), 1)
if focus_average >= 8:
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."
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": _categories_for_request(request),
"weekly_trend": weekly_trend,
"weekly_summary": _build_weekly_summary(weekly_trend),
"is_demo_mode": not request.user.is_authenticated,
"stats": {
"total_entries": totals["total_entries"],
"avg_focus": focus_average,
"avg_energy": energy_average,
"total_minutes": totals["total_minutes"],
"active_days": active_days,
"top_category": top_category["category__name"] if top_category else "No category yet",
"spotlight": spotlight,
},
}
def home(request): def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower() host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now() 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(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:
form = MomentumEntryForm()
context = { context = {
"project_name": "New Style", "project_name": APP_NAME,
"agent_brand": agent_brand, "agent_brand": agent_brand,
"django_version": django_version(), "django_version": django_version(),
"python_version": platform.python_version(), "python_version": platform.python_version(),
"current_time": now, "current_time": now,
"host_name": host_name, "host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_description": os.getenv("PROJECT_DESCRIPTION", APP_TAGLINE),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
"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(request),
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
def entry_list(request):
selected_slug = request.GET.get("category", "")
entries = _entries_for_request(request)
if selected_slug:
entries = entries.filter(category__slug=selected_slug)
context = {
"page_title": f"All check-ins | {APP_NAME}",
"meta_description": "Browse recent check-ins and filter your momentum history by category.",
"entries": entries,
"categories": _categories_for_request(request),
"selected_slug": selected_slug,
"is_demo_mode": not request.user.is_authenticated,
"history_overview": _build_history_overview(entries),
"recent_activity": _build_recent_activity(entries),
"category_breakdown": _build_category_breakdown(entries),
}
return render(request, "core/entry_list.html", context)
def entry_detail(request, pk):
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

@ -1,4 +1,816 @@
/* Custom styles for the application */ /* Momentum Atlas theme */
body { :root {
font-family: system-ui, -apple-system, sans-serif; --brand-ink: #0f172a;
--brand-muted: #475569;
--brand-surface: #fffaf4;
--brand-surface-strong: #ffffff;
--brand-border: rgba(15, 23, 42, 0.08);
--brand-primary: #0f766e;
--brand-primary-dark: #115e59;
--brand-secondary: #f59e0b;
--brand-accent: #f97316;
--brand-highlight: #14b8a6;
--brand-glow: rgba(249, 115, 22, 0.18);
--brand-shadow: 0 28px 80px rgba(15, 23, 42, 0.12);
--radius-xl: 28px;
--radius-lg: 20px;
--radius-md: 16px;
--section-gap: clamp(4rem, 8vw, 7rem);
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--brand-ink);
background:
radial-gradient(circle at top left, rgba(20, 184, 166, 0.14), transparent 28%),
radial-gradient(circle at bottom right, rgba(245, 158, 11, 0.14), transparent 30%),
linear-gradient(180deg, #fffdf8 0%, #fff7ed 46%, #f8fafc 100%);
min-height: 100vh;
}
h1,
h2,
h3,
h4,
.brand-mark {
font-family: 'Manrope', 'Inter', sans-serif;
letter-spacing: -0.03em;
}
p {
color: var(--brand-muted);
line-height: 1.7;
}
.site-shell,
.subpage-shell {
position: relative;
overflow: hidden;
}
.site-header {
position: relative;
}
.main-nav {
background: rgba(15, 23, 42, 0.32);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.brand-mark {
color: #ffffff;
font-size: 1.35rem;
font-weight: 800;
}
.brand-mark:hover,
.brand-mark:focus,
.nav-link:hover,
.nav-link:focus {
color: #fff7ed;
}
.nav-link {
color: rgba(255, 255, 255, 0.82);
font-weight: 600;
}
.nav-pill {
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
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;
background: linear-gradient(135deg, #0f172a 0%, #0f766e 48%, #f97316 100%);
color: #ffffff;
}
.hero-orb {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: 0.75;
}
.hero-orb-one {
width: 260px;
height: 260px;
top: 7rem;
right: 8%;
background: rgba(20, 184, 166, 0.32);
}
.hero-orb-two {
width: 190px;
height: 190px;
bottom: 3rem;
left: 6%;
background: rgba(245, 158, 11, 0.28);
}
.hero-title {
font-size: clamp(2.8rem, 5vw, 5rem);
line-height: 0.98;
margin: 0 0 1.5rem;
}
.hero-copy {
color: rgba(255, 255, 255, 0.86);
font-size: 1.1rem;
max-width: 40rem;
}
.eyebrow {
display: inline-block;
margin-bottom: 1rem;
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--brand-accent);
}
.hero-section .eyebrow,
.hero-section .hero-meta-label {
color: rgba(255, 247, 237, 0.78);
}
.hero-actions,
.hero-meta {
position: relative;
z-index: 2;
}
.hero-meta strong {
display: block;
font-size: 1rem;
}
.hero-meta-label,
.panel-label,
.form-hint {
font-size: 0.82rem;
color: #64748b;
}
.glass-panel {
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(255, 255, 255, 0.42);
border-radius: var(--radius-xl);
box-shadow: var(--brand-shadow);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.insight-panel {
padding: 2rem;
}
.metric-card {
height: 100%;
padding: 1.2rem;
border-radius: var(--radius-md);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 247, 237, 0.76));
border: 1px solid var(--brand-border);
}
.metric-card span {
display: block;
font-size: 0.88rem;
color: var(--brand-muted);
}
.metric-card strong {
font-size: clamp(1.55rem, 3vw, 2rem);
color: var(--brand-ink);
}
.spotlight-note {
padding: 1.1rem 1.2rem;
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.66);
border: 1px solid rgba(15, 23, 42, 0.06);
}
.spotlight-note p {
margin: 0.45rem 0 0.8rem;
}
.chip-row,
.filter-row,
.message-stack {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.trend-chip,
.filter-chip,
.category-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.55rem 0.9rem;
border-radius: 999px;
font-weight: 700;
font-size: 0.88rem;
text-decoration: none;
}
.trend-chip,
.category-badge {
background: rgba(15, 118, 110, 0.1);
color: var(--brand-primary-dark);
}
.filter-chip {
background: rgba(255, 255, 255, 0.78);
color: var(--brand-ink);
border: 1px solid var(--brand-border);
}
.filter-chip.active,
.filter-chip:hover,
.filter-chip:focus {
background: var(--brand-primary);
color: #ffffff;
border-color: transparent;
}
.section-block {
padding: var(--section-gap) 0;
}
.section-muted {
background: rgba(255, 255, 255, 0.42);
}
.section-heading h2,
.subpage-header h1,
.detail-panel h1,
.auth-card h1 {
font-size: clamp(2rem, 3vw, 3rem);
margin-bottom: 0.8rem;
}
.section-heading.compact h2 {
font-size: clamp(1.6rem, 2vw, 2.2rem);
}
.form-panel,
.chart-panel,
.detail-panel,
.sidebar-panel,
.subpage-header,
.empty-state,
.auth-card {
padding: 2rem;
}
.form-panel .form-control,
.form-panel .form-select,
.auth-card .form-control {
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.35);
padding: 0.9rem 1rem;
min-height: 3.3rem;
background: rgba(255, 255, 255, 0.92);
}
.form-panel textarea.form-control {
min-height: 8.5rem;
}
.form-control:focus,
.form-select:focus,
.btn:focus,
.btn:active:focus,
.related-item:focus,
.entry-card a:focus {
box-shadow: 0 0 0 0.28rem rgba(20, 184, 166, 0.2);
border-color: rgba(20, 184, 166, 0.6);
}
.form-label {
font-weight: 700;
margin-bottom: 0.55rem;
}
.btn {
border-radius: 999px;
padding: 0.9rem 1.35rem;
font-weight: 700;
border: none;
}
.btn-accent {
background: linear-gradient(135deg, var(--brand-secondary), var(--brand-accent));
color: #ffffff;
box-shadow: 0 18px 40px var(--brand-glow);
}
.btn-accent:hover,
.btn-accent:focus {
color: #ffffff;
transform: translateY(-1px);
}
.btn-ghost {
background: rgba(255, 255, 255, 0.18);
color: var(--brand-ink);
border: 1px solid rgba(15, 23, 42, 0.12);
}
.hero-section .btn-ghost {
color: #ffffff;
border-color: rgba(255, 255, 255, 0.28);
background: rgba(255, 255, 255, 0.12);
}
.stack-grid {
display: grid;
gap: 1rem;
}
.feature-card {
padding: 1.5rem;
}
.feature-card h3 {
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));
gap: 1rem;
align-items: end;
}
.trend-day {
text-align: center;
}
.trend-bars {
display: flex;
align-items: end;
justify-content: center;
gap: 0.45rem;
height: 220px;
margin-bottom: 1rem;
}
.trend-bar {
width: 22px;
border-radius: 999px 999px 12px 12px;
min-height: 14px;
box-shadow: inset 0 -8px 18px rgba(255, 255, 255, 0.18);
}
.trend-bar.focus {
background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight));
}
.trend-bar.energy {
background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent));
}
.trend-bar.level-0 { height: 14px; }
.trend-bar.level-10 { height: 10%; }
.trend-bar.level-20 { height: 20%; }
.trend-bar.level-30 { height: 30%; }
.trend-bar.level-40 { height: 40%; }
.trend-bar.level-50 { height: 50%; }
.trend-bar.level-60 { height: 60%; }
.trend-bar.level-70 { height: 70%; }
.trend-bar.level-80 { height: 80%; }
.trend-bar.level-90 { height: 90%; }
.trend-bar.level-100 { height: 100%; }
.trend-values,
.entry-date,
.related-item span {
color: var(--brand-muted);
font-size: 0.9rem;
}
.entry-card {
padding: 1.5rem;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.entry-card:hover,
.related-item:hover {
transform: translateY(-4px);
}
.entry-card-top {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.entry-card h3,
.entry-card h2 {
margin: 1rem 0 0.75rem;
font-size: 1.35rem;
}
.entry-card a,
.text-link,
.related-item {
color: var(--brand-ink);
text-decoration: none;
}
.text-link {
font-weight: 700;
}
.entry-stats,
.detail-metrics {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.2rem;
}
.entry-stats span {
padding: 0.45rem 0.7rem;
border-radius: 999px;
background: rgba(248, 250, 252, 0.95);
border: 1px solid rgba(148, 163, 184, 0.18);
font-size: 0.88rem;
}
.detail-panel .detail-lead {
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.detail-metric {
min-width: 140px;
flex: 1 1 0;
}
.reflection-block {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.related-list {
display: grid;
gap: 0.85rem;
}
.related-item {
display: block;
padding: 1rem;
border-radius: 16px;
background: rgba(255, 255, 255, 0.74);
border: 1px solid var(--brand-border);
}
.related-item strong {
display: block;
margin-bottom: 0.35rem;
}
.custom-alert {
border: none;
border-radius: 18px;
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
}
.empty-state {
text-align: center;
}
.subpage-shell {
padding: 1.5rem 0 3rem;
}
.subpage-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.5rem;
align-items: center;
}
@media (max-width: 991.98px) {
.hero-section {
padding-bottom: 3rem;
}
.trend-chart {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 767.98px) {
.hero-title {
font-size: 2.8rem;
}
.trend-chart {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.trend-bars {
height: 180px;
}
.form-panel,
.chart-panel,
.detail-panel,
.sidebar-panel,
.subpage-header,
.empty-state,
.insight-panel,
.auth-card {
padding: 1.4rem;
}
.nav-user {
width: fit-content;
}
}
/* Step 3 history + chart polish */
.chart-panel-top {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.chart-panel-top-inline {
align-items: center;
}
.chart-intro {
max-width: 620px;
color: var(--brand-muted);
}
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
}
.legend-pill {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 0.8rem;
border-radius: 999px;
background: rgba(248, 250, 252, 0.92);
border: 1px solid var(--brand-border);
font-size: 0.88rem;
color: var(--brand-ink);
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.legend-dot.focus {
background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight));
}
.legend-dot.energy {
background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent));
}
.trend-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.trend-summary-card,
.summary-stat-card {
background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--brand-border);
border-radius: 24px;
padding: 1.2rem 1.25rem;
}
.trend-summary-card span,
.summary-stat-card span,
.mini-meter span,
.lane-meta span,
.score-track-item span {
color: var(--brand-muted);
font-size: 0.88rem;
}
.trend-summary-card strong,
.summary-stat-card strong {
display: block;
font-size: 1.4rem;
margin-top: 0.35rem;
}
.summary-stat-card p {
margin: 0.65rem 0 0;
color: var(--brand-muted);
}
.activity-list,
.lane-list {
display: grid;
gap: 1rem;
}
.activity-row,
.lane-card {
padding: 1.15rem;
border-radius: 20px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--brand-border);
}
.activity-row-head,
.lane-card-top,
.entry-card-footer,
.lane-meta {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.activity-row-head h3 {
margin: 0.45rem 0 0;
font-size: 1.05rem;
}
.score-track-group {
display: grid;
gap: 0.7rem;
margin-top: 1rem;
}
.score-track-item {
display: grid;
grid-template-columns: 72px minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
}
.score-track {
width: 100%;
height: 10px;
border-radius: 999px;
background: rgba(203, 213, 225, 0.5);
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: inherit;
}
.score-fill.focus {
background: linear-gradient(90deg, var(--brand-primary), var(--brand-highlight));
}
.score-fill.energy {
background: linear-gradient(90deg, var(--brand-secondary), var(--brand-accent));
}
.score-fill.minutes {
background: linear-gradient(90deg, var(--brand-ink), #334155);
}
.lane-card-top strong {
display: block;
}
.lane-card-top span,
.lane-meta a {
color: var(--brand-muted);
text-decoration: none;
}
.lane-track {
height: 12px;
border-radius: 999px;
background: rgba(203, 213, 225, 0.45);
overflow: hidden;
margin: 0.95rem 0 0.8rem;
}
.lane-fill {
height: 100%;
border-radius: inherit;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
}
.entry-card-enhanced {
position: relative;
overflow: hidden;
}
.entry-card-enhanced::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 4px;
background: linear-gradient(90deg, var(--entry-accent, var(--brand-primary)), var(--brand-highlight));
}
.entry-card-footer {
margin-top: 1.2rem;
padding-top: 1rem;
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.mini-meter strong {
display: block;
font-size: 1rem;
margin-top: 0.25rem;
}
@media (max-width: 991.98px) {
.trend-summary-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 767.98px) {
.chart-panel-top,
.activity-row-head,
.lane-card-top,
.entry-card-footer,
.lane-meta {
flex-direction: column;
align-items: flex-start;
}
.score-track-item {
grid-template-columns: 1fr;
}
} }

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

@ -1,21 +1,816 @@
/* Momentum Atlas theme */
:root { :root {
--bg-color-start: #6a11cb; --brand-ink: #0f172a;
--bg-color-end: #2575fc; --brand-muted: #475569;
--text-color: #ffffff; --brand-surface: #fffaf4;
--card-bg-color: rgba(255, 255, 255, 0.01); --brand-surface-strong: #ffffff;
--card-border-color: rgba(255, 255, 255, 0.1); --brand-border: rgba(15, 23, 42, 0.08);
--brand-primary: #0f766e;
--brand-primary-dark: #115e59;
--brand-secondary: #f59e0b;
--brand-accent: #f97316;
--brand-highlight: #14b8a6;
--brand-glow: rgba(249, 115, 22, 0.18);
--brand-shadow: 0 28px 80px rgba(15, 23, 42, 0.12);
--radius-xl: 28px;
--radius-lg: 20px;
--radius-md: 16px;
--section-gap: clamp(4rem, 8vw, 7rem);
} }
html {
scroll-behavior: smooth;
}
body { body {
margin: 0; margin: 0;
font-family: 'Inter', sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); color: var(--brand-ink);
color: var(--text-color); background:
display: flex; radial-gradient(circle at top left, rgba(20, 184, 166, 0.14), transparent 28%),
justify-content: center; radial-gradient(circle at bottom right, rgba(245, 158, 11, 0.14), transparent 30%),
align-items: center; linear-gradient(180deg, #fffdf8 0%, #fff7ed 46%, #f8fafc 100%);
min-height: 100vh; min-height: 100vh;
text-align: center; }
h1,
h2,
h3,
h4,
.brand-mark {
font-family: 'Manrope', 'Inter', sans-serif;
letter-spacing: -0.03em;
}
p {
color: var(--brand-muted);
line-height: 1.7;
}
.site-shell,
.subpage-shell {
position: relative;
overflow: hidden; overflow: hidden;
}
.site-header {
position: relative; position: relative;
} }
.main-nav {
background: rgba(15, 23, 42, 0.32);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.brand-mark {
color: #ffffff;
font-size: 1.35rem;
font-weight: 800;
}
.brand-mark:hover,
.brand-mark:focus,
.nav-link:hover,
.nav-link:focus {
color: #fff7ed;
}
.nav-link {
color: rgba(255, 255, 255, 0.82);
font-weight: 600;
}
.nav-pill {
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
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;
background: linear-gradient(135deg, #0f172a 0%, #0f766e 48%, #f97316 100%);
color: #ffffff;
}
.hero-orb {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: 0.75;
}
.hero-orb-one {
width: 260px;
height: 260px;
top: 7rem;
right: 8%;
background: rgba(20, 184, 166, 0.32);
}
.hero-orb-two {
width: 190px;
height: 190px;
bottom: 3rem;
left: 6%;
background: rgba(245, 158, 11, 0.28);
}
.hero-title {
font-size: clamp(2.8rem, 5vw, 5rem);
line-height: 0.98;
margin: 0 0 1.5rem;
}
.hero-copy {
color: rgba(255, 255, 255, 0.86);
font-size: 1.1rem;
max-width: 40rem;
}
.eyebrow {
display: inline-block;
margin-bottom: 1rem;
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--brand-accent);
}
.hero-section .eyebrow,
.hero-section .hero-meta-label {
color: rgba(255, 247, 237, 0.78);
}
.hero-actions,
.hero-meta {
position: relative;
z-index: 2;
}
.hero-meta strong {
display: block;
font-size: 1rem;
}
.hero-meta-label,
.panel-label,
.form-hint {
font-size: 0.82rem;
color: #64748b;
}
.glass-panel {
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(255, 255, 255, 0.42);
border-radius: var(--radius-xl);
box-shadow: var(--brand-shadow);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.insight-panel {
padding: 2rem;
}
.metric-card {
height: 100%;
padding: 1.2rem;
border-radius: var(--radius-md);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 247, 237, 0.76));
border: 1px solid var(--brand-border);
}
.metric-card span {
display: block;
font-size: 0.88rem;
color: var(--brand-muted);
}
.metric-card strong {
font-size: clamp(1.55rem, 3vw, 2rem);
color: var(--brand-ink);
}
.spotlight-note {
padding: 1.1rem 1.2rem;
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.66);
border: 1px solid rgba(15, 23, 42, 0.06);
}
.spotlight-note p {
margin: 0.45rem 0 0.8rem;
}
.chip-row,
.filter-row,
.message-stack {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.trend-chip,
.filter-chip,
.category-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.55rem 0.9rem;
border-radius: 999px;
font-weight: 700;
font-size: 0.88rem;
text-decoration: none;
}
.trend-chip,
.category-badge {
background: rgba(15, 118, 110, 0.1);
color: var(--brand-primary-dark);
}
.filter-chip {
background: rgba(255, 255, 255, 0.78);
color: var(--brand-ink);
border: 1px solid var(--brand-border);
}
.filter-chip.active,
.filter-chip:hover,
.filter-chip:focus {
background: var(--brand-primary);
color: #ffffff;
border-color: transparent;
}
.section-block {
padding: var(--section-gap) 0;
}
.section-muted {
background: rgba(255, 255, 255, 0.42);
}
.section-heading h2,
.subpage-header h1,
.detail-panel h1,
.auth-card h1 {
font-size: clamp(2rem, 3vw, 3rem);
margin-bottom: 0.8rem;
}
.section-heading.compact h2 {
font-size: clamp(1.6rem, 2vw, 2.2rem);
}
.form-panel,
.chart-panel,
.detail-panel,
.sidebar-panel,
.subpage-header,
.empty-state,
.auth-card {
padding: 2rem;
}
.form-panel .form-control,
.form-panel .form-select,
.auth-card .form-control {
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.35);
padding: 0.9rem 1rem;
min-height: 3.3rem;
background: rgba(255, 255, 255, 0.92);
}
.form-panel textarea.form-control {
min-height: 8.5rem;
}
.form-control:focus,
.form-select:focus,
.btn:focus,
.btn:active:focus,
.related-item:focus,
.entry-card a:focus {
box-shadow: 0 0 0 0.28rem rgba(20, 184, 166, 0.2);
border-color: rgba(20, 184, 166, 0.6);
}
.form-label {
font-weight: 700;
margin-bottom: 0.55rem;
}
.btn {
border-radius: 999px;
padding: 0.9rem 1.35rem;
font-weight: 700;
border: none;
}
.btn-accent {
background: linear-gradient(135deg, var(--brand-secondary), var(--brand-accent));
color: #ffffff;
box-shadow: 0 18px 40px var(--brand-glow);
}
.btn-accent:hover,
.btn-accent:focus {
color: #ffffff;
transform: translateY(-1px);
}
.btn-ghost {
background: rgba(255, 255, 255, 0.18);
color: var(--brand-ink);
border: 1px solid rgba(15, 23, 42, 0.12);
}
.hero-section .btn-ghost {
color: #ffffff;
border-color: rgba(255, 255, 255, 0.28);
background: rgba(255, 255, 255, 0.12);
}
.stack-grid {
display: grid;
gap: 1rem;
}
.feature-card {
padding: 1.5rem;
}
.feature-card h3 {
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));
gap: 1rem;
align-items: end;
}
.trend-day {
text-align: center;
}
.trend-bars {
display: flex;
align-items: end;
justify-content: center;
gap: 0.45rem;
height: 220px;
margin-bottom: 1rem;
}
.trend-bar {
width: 22px;
border-radius: 999px 999px 12px 12px;
min-height: 14px;
box-shadow: inset 0 -8px 18px rgba(255, 255, 255, 0.18);
}
.trend-bar.focus {
background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight));
}
.trend-bar.energy {
background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent));
}
.trend-bar.level-0 { height: 14px; }
.trend-bar.level-10 { height: 10%; }
.trend-bar.level-20 { height: 20%; }
.trend-bar.level-30 { height: 30%; }
.trend-bar.level-40 { height: 40%; }
.trend-bar.level-50 { height: 50%; }
.trend-bar.level-60 { height: 60%; }
.trend-bar.level-70 { height: 70%; }
.trend-bar.level-80 { height: 80%; }
.trend-bar.level-90 { height: 90%; }
.trend-bar.level-100 { height: 100%; }
.trend-values,
.entry-date,
.related-item span {
color: var(--brand-muted);
font-size: 0.9rem;
}
.entry-card {
padding: 1.5rem;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.entry-card:hover,
.related-item:hover {
transform: translateY(-4px);
}
.entry-card-top {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.entry-card h3,
.entry-card h2 {
margin: 1rem 0 0.75rem;
font-size: 1.35rem;
}
.entry-card a,
.text-link,
.related-item {
color: var(--brand-ink);
text-decoration: none;
}
.text-link {
font-weight: 700;
}
.entry-stats,
.detail-metrics {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.2rem;
}
.entry-stats span {
padding: 0.45rem 0.7rem;
border-radius: 999px;
background: rgba(248, 250, 252, 0.95);
border: 1px solid rgba(148, 163, 184, 0.18);
font-size: 0.88rem;
}
.detail-panel .detail-lead {
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.detail-metric {
min-width: 140px;
flex: 1 1 0;
}
.reflection-block {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.related-list {
display: grid;
gap: 0.85rem;
}
.related-item {
display: block;
padding: 1rem;
border-radius: 16px;
background: rgba(255, 255, 255, 0.74);
border: 1px solid var(--brand-border);
}
.related-item strong {
display: block;
margin-bottom: 0.35rem;
}
.custom-alert {
border: none;
border-radius: 18px;
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
}
.empty-state {
text-align: center;
}
.subpage-shell {
padding: 1.5rem 0 3rem;
}
.subpage-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.5rem;
align-items: center;
}
@media (max-width: 991.98px) {
.hero-section {
padding-bottom: 3rem;
}
.trend-chart {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 767.98px) {
.hero-title {
font-size: 2.8rem;
}
.trend-chart {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.trend-bars {
height: 180px;
}
.form-panel,
.chart-panel,
.detail-panel,
.sidebar-panel,
.subpage-header,
.empty-state,
.insight-panel,
.auth-card {
padding: 1.4rem;
}
.nav-user {
width: fit-content;
}
}
/* Step 3 history + chart polish */
.chart-panel-top {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.chart-panel-top-inline {
align-items: center;
}
.chart-intro {
max-width: 620px;
color: var(--brand-muted);
}
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
}
.legend-pill {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 0.8rem;
border-radius: 999px;
background: rgba(248, 250, 252, 0.92);
border: 1px solid var(--brand-border);
font-size: 0.88rem;
color: var(--brand-ink);
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.legend-dot.focus {
background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight));
}
.legend-dot.energy {
background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent));
}
.trend-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.trend-summary-card,
.summary-stat-card {
background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--brand-border);
border-radius: 24px;
padding: 1.2rem 1.25rem;
}
.trend-summary-card span,
.summary-stat-card span,
.mini-meter span,
.lane-meta span,
.score-track-item span {
color: var(--brand-muted);
font-size: 0.88rem;
}
.trend-summary-card strong,
.summary-stat-card strong {
display: block;
font-size: 1.4rem;
margin-top: 0.35rem;
}
.summary-stat-card p {
margin: 0.65rem 0 0;
color: var(--brand-muted);
}
.activity-list,
.lane-list {
display: grid;
gap: 1rem;
}
.activity-row,
.lane-card {
padding: 1.15rem;
border-radius: 20px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--brand-border);
}
.activity-row-head,
.lane-card-top,
.entry-card-footer,
.lane-meta {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.activity-row-head h3 {
margin: 0.45rem 0 0;
font-size: 1.05rem;
}
.score-track-group {
display: grid;
gap: 0.7rem;
margin-top: 1rem;
}
.score-track-item {
display: grid;
grid-template-columns: 72px minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
}
.score-track {
width: 100%;
height: 10px;
border-radius: 999px;
background: rgba(203, 213, 225, 0.5);
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: inherit;
}
.score-fill.focus {
background: linear-gradient(90deg, var(--brand-primary), var(--brand-highlight));
}
.score-fill.energy {
background: linear-gradient(90deg, var(--brand-secondary), var(--brand-accent));
}
.score-fill.minutes {
background: linear-gradient(90deg, var(--brand-ink), #334155);
}
.lane-card-top strong {
display: block;
}
.lane-card-top span,
.lane-meta a {
color: var(--brand-muted);
text-decoration: none;
}
.lane-track {
height: 12px;
border-radius: 999px;
background: rgba(203, 213, 225, 0.45);
overflow: hidden;
margin: 0.95rem 0 0.8rem;
}
.lane-fill {
height: 100%;
border-radius: inherit;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
}
.entry-card-enhanced {
position: relative;
overflow: hidden;
}
.entry-card-enhanced::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 4px;
background: linear-gradient(90deg, var(--entry-accent, var(--brand-primary)), var(--brand-highlight));
}
.entry-card-footer {
margin-top: 1.2rem;
padding-top: 1rem;
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.mini-meter strong {
display: block;
font-size: 1rem;
margin-top: 0.25rem;
}
@media (max-width: 991.98px) {
.trend-summary-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 767.98px) {
.chart-panel-top,
.activity-row-head,
.lane-card-top,
.entry-card-footer,
.lane-meta {
flex-direction: column;
align-items: flex-start;
}
.score-track-item {
grid-template-columns: 1fr;
}
}

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