Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -18,10 +18,8 @@ from django.contrib import admin
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.views.generic.base import RedirectView
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("favicon.ico", RedirectView.as_view(url=settings.STATIC_URL + "images/favicon.svg", permanent=False)),
|
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,26 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Category, MomentumEntry
|
# Register your models here.
|
||||||
|
|
||||||
|
|
||||||
@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
104
core/forms.py
@ -1,104 +0,0 @@
|
|||||||
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 today’s 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
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
# 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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
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),
|
|
||||||
]
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-04-16 11:22
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0002_seed_demo_data'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='momentumentry',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='momentum_entries', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,55 +1,3 @@
|
|||||||
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])
|
|
||||||
|
|||||||
@ -1,30 +1,25 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
{% load static %}
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||||
<title>{% block title %}{{ page_title|default:"Momentum Atlas" }}{% endblock %}</title>
|
{% if project_description %}
|
||||||
<meta name="description" content="{% block meta_description %}{{ meta_description|default:project_description }}{% endblock %}">
|
<meta name="description" content="{{ project_description }}">
|
||||||
<meta name="keywords" content="productivity dashboard, personal tracker, python django app">
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
<meta name="author" content="Flatlogic">
|
<meta property="twitter:description" content="{{ project_description }}">
|
||||||
|
{% 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 %}
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
{% load static %}
|
||||||
<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>
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,307 +1,145 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ page_title }}{% endblock %}
|
{% block title %}{{ project_name }}{% 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 %}
|
||||||
<div class="site-shell">
|
|
||||||
<header class="site-header">
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark main-nav">
|
|
||||||
<div class="container py-3">
|
|
||||||
<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>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<section class="hero-section">
|
|
||||||
<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 today’s 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>
|
<main>
|
||||||
<section class="section-block" id="check-in">
|
<div class="card">
|
||||||
<div class="container">
|
<h1>Analyzing your requirements and generating your app…</h1>
|
||||||
{% if messages %}
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<div class="message-stack mb-4">
|
<span class="sr-only">Loading…</span>
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert alert-{{ message.tags|default:'info' }} custom-alert mb-0" role="alert">{{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||||
<div class="row g-4 align-items-start">
|
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||||
<div class="col-xl-7">
|
<p class="runtime">
|
||||||
<div class="section-heading">
|
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||||
<span class="eyebrow">Workflow widget</span>
|
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||||
<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>
|
||||||
<div class="glass-panel form-panel">
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<form method="post" novalidate>
|
|
||||||
{% 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>
|
</main>
|
||||||
</div>
|
<footer>
|
||||||
|
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||||
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,58 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,37 +1,3 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from .models import Category, MomentumEntry
|
# Create your tests here.
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
17
core/urls.py
17
core/urls.py
@ -1,22 +1,7 @@
|
|||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .forms import LoginForm
|
from .views import home
|
||||||
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"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
327
core/views.py
327
core/views.py
@ -1,344 +1,25 @@
|
|||||||
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.contrib import messages
|
from django.shortcuts import render
|
||||||
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": APP_NAME,
|
"project_name": "New Style",
|
||||||
"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", APP_TAGLINE),
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"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)
|
|
||||||
|
|||||||
@ -1,816 +1,4 @@
|
|||||||
/* Momentum Atlas theme */
|
/* Custom styles for the application */
|
||||||
:root {
|
|
||||||
--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 {
|
body {
|
||||||
margin: 0;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,816 +1,21 @@
|
|||||||
/* Momentum Atlas theme */
|
|
||||||
:root {
|
:root {
|
||||||
--brand-ink: #0f172a;
|
--bg-color-start: #6a11cb;
|
||||||
--brand-muted: #475569;
|
--bg-color-end: #2575fc;
|
||||||
--brand-surface: #fffaf4;
|
--text-color: #ffffff;
|
||||||
--brand-surface-strong: #ffffff;
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
--brand-border: rgba(15, 23, 42, 0.08);
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
--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', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
color: var(--brand-ink);
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
background:
|
color: var(--text-color);
|
||||||
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;
|
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;
|
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;
|
align-items: center;
|
||||||
}
|
min-height: 100vh;
|
||||||
|
|
||||||
.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;
|
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;
|
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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
Loading…
x
Reference in New Issue
Block a user