diff --git a/.gitignore b/.gitignore index 7db2825..bcce168 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,8 @@ cover/ local_settings.py db.sqlite3 db.sqlite3-journal +staticfiles/ +app/staticfiles/ # Flask stuff: instance/ diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index 99463bb..0000000 --- a/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Application package for StudyBuddy domain apps.""" diff --git a/apps/users/forms.py b/apps/users/forms.py index 1f6ea3b..b1c4876 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -1,15 +1,78 @@ -"""Forms for StudyBuddy user accounts.""" +"""Forms for StudyBuddy user registration and account workflows.""" from __future__ import annotations +from django import forms +from django.contrib.auth import get_user_model from django.contrib.auth.forms import UserCreationForm -from apps.users.models import CustomUser +CustomUser = get_user_model() -class CustomUserCreationForm(UserCreationForm): - """Signup form for the custom user model.""" +class UserSignUpForm(UserCreationForm): + """Form used by visitors to create a StudyBuddy account.""" + + email = forms.EmailField( + label="Email address", + help_text="Use this email address when signing in.", + ) + username = forms.CharField( + required=False, + help_text="Optional. If left blank, StudyBuddy derives one from your email.", + ) + + class Meta: + """Form metadata for user signup.""" - class Meta(UserCreationForm.Meta): model = CustomUser - fields = ("email", "username") + fields = ( + "email", + "username", + "first_name", + "last_name", + "password1", + "password2", + ) + + def clean_email(self) -> str: + """Validate that the submitted email is unique.""" + email = self.cleaned_data["email"].strip().lower() + + if CustomUser.objects.filter(email__iexact=email).exists(): + raise forms.ValidationError("A user with this email already exists.") + + return email + + def clean_username(self) -> str: + """Validate an optional username only when the user supplies one.""" + username = self.cleaned_data.get("username", "").strip() + + if username and CustomUser.objects.filter(username__iexact=username).exists(): + raise forms.ValidationError("A user with this username already exists.") + + return username + + def save(self, commit: bool = True): + """Create a user with a safe username and email-first login.""" + user = super().save(commit=False) + user.email = self.cleaned_data["email"] + user.username = self.cleaned_data.get("username") or self._unique_username() + + if commit: + user.save() + self.save_m2m() + + return user + + def _unique_username(self) -> str: + """Build a unique username from the email local part.""" + email = self.cleaned_data["email"] + base_username = email.split("@", maxsplit=1)[0][:140] or "user" + candidate = base_username + suffix = 1 + + while CustomUser.objects.filter(username__iexact=candidate).exists(): + suffix += 1 + candidate = f"{base_username}-{suffix}" + + return candidate diff --git a/apps/users/tests/test_auth_views.py b/apps/users/tests/test_auth_views.py new file mode 100644 index 0000000..9e8fedc --- /dev/null +++ b/apps/users/tests/test_auth_views.py @@ -0,0 +1,170 @@ +"""Integration tests for user authentication views.""" + +from __future__ import annotations + +import pytest +from django.contrib.auth import get_user_model +from django.urls import reverse + +from apps.users.factories import CustomUserFactory +from apps.users.forms import UserSignUpForm + +CustomUser = get_user_model() + + +@pytest.mark.django_db +def test_signup_page_renders(client): + """The signup page renders a registration form.""" + response = client.get(reverse("users:signup")) + + assert response.status_code == 200 + assert b"Create Account" in response.content + assert b"New Account" in response.content + + +@pytest.mark.django_db +def test_signup_creates_user_and_redirects_to_dashboard(client): + """A valid signup creates a user and sends them to the dashboard.""" + response = client.post( + reverse("users:signup"), + { + "email": "new.user@example.com", + "username": "", + "first_name": "New", + "last_name": "User", + "password1": "StrongPassword123!", + "password2": "StrongPassword123!", + }, + ) + + assert response.status_code == 302 + assert response["Location"] == reverse("dashboard:index") + assert CustomUser.objects.filter(email="new.user@example.com").exists() + + +@pytest.mark.django_db +def test_signup_rejects_duplicate_email(client): + """Signup rejects email addresses that already belong to another user.""" + CustomUserFactory(email="duplicate@example.com") + + response = client.post( + reverse("users:signup"), + { + "email": "duplicate@example.com", + "username": "duplicate-user", + "first_name": "Duplicate", + "last_name": "User", + "password1": "StrongPassword123!", + "password2": "StrongPassword123!", + }, + ) + + assert response.status_code == 200 + assert b"A user with this email already exists." in response.content + + +@pytest.mark.django_db +def test_signup_rejects_duplicate_username(client): + """Signup rejects explicitly supplied usernames already in use.""" + CustomUserFactory(username="existing-user") + + response = client.post( + reverse("users:signup"), + { + "email": "unique@example.com", + "username": "existing-user", + "first_name": "Unique", + "last_name": "User", + "password1": "StrongPassword123!", + "password2": "StrongPassword123!", + }, + ) + + assert response.status_code == 200 + assert b"A user with this username already exists." in response.content + + +@pytest.mark.django_db +def test_signup_generates_unique_username_when_email_local_part_exists(client): + """Blank usernames are derived from email and made unique.""" + CustomUserFactory(email="learner@example.com", username="learner") + + response = client.post( + reverse("users:signup"), + { + "email": "learner@studybuddy.test", + "username": "", + "first_name": "New", + "last_name": "Learner", + "password1": "StrongPassword123!", + "password2": "StrongPassword123!", + }, + ) + + assert response.status_code == 302 + user = CustomUser.objects.get(email="learner@studybuddy.test") + + assert user.username == "learner-2" + + +@pytest.mark.django_db +def test_signup_form_save_commit_false_builds_unsaved_user(): + """Signup form can build a valid custom user without persisting it.""" + form = UserSignUpForm( + data={ + "email": "draft@example.com", + "username": "", + "first_name": "Draft", + "last_name": "User", + "password1": "StrongPassword123!", + "password2": "StrongPassword123!", + } + ) + + assert form.is_valid() + + user = form.save(commit=False) + + assert user.pk is None + assert user.email == "draft@example.com" + assert user.username == "draft" + assert user.check_password("StrongPassword123!") + assert not CustomUser.objects.filter(email="draft@example.com").exists() + + +@pytest.mark.django_db +def test_login_with_email_redirects_to_dashboard(client): + """Users can log in with their email address and password.""" + user = CustomUserFactory(email="login@example.com") + + response = client.post( + reverse("users:login"), + { + "username": user.email, + "password": "password123", + }, + ) + + assert response.status_code == 302 + assert response["Location"] == reverse("dashboard:index") + + +@pytest.mark.django_db +def test_profile_requires_login(client): + """Anonymous users are redirected before viewing a profile.""" + response = client.get(reverse("users:profile")) + + assert response.status_code == 302 + assert response["Location"].startswith(f"{reverse('users:login')}?next=") + + +@pytest.mark.django_db +def test_authenticated_profile_shows_user_email(client): + """Authenticated users can view their account profile.""" + user = CustomUserFactory(email="profile@example.com") + client.force_login(user) + + response = client.get(reverse("users:profile")) + + assert response.status_code == 200 + assert b"profile@example.com" in response.content diff --git a/apps/users/views.py b/apps/users/views.py index 8d863ab..92b9ae9 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -1,29 +1,31 @@ -"""Views for StudyBuddy user accounts.""" +"""Views for StudyBuddy account workflows.""" from __future__ import annotations from django.contrib.auth import login from django.contrib.auth.decorators import login_required +from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, render -from apps.users.forms import CustomUserCreationForm +from apps.users.forms import UserSignUpForm -def signup(request): - """Register a new StudyBuddy user.""" +def signup(request: HttpRequest) -> HttpResponse: + """Register a new user and send them to the dashboard.""" if request.method == "POST": - form = CustomUserCreationForm(request.POST) + form = UserSignUpForm(request.POST) + if form.is_valid(): user = form.save() - login(request, user) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") return redirect("dashboard:index") else: - form = CustomUserCreationForm() + form = UserSignUpForm() - return render(request, "registration/signup.html", {"form": form}) + return render(request, "users/signup.html", {"form": form}) @login_required -def profile(request): - """Render the signed-in user's profile page.""" +def profile(request: HttpRequest) -> HttpResponse: + """Render the authenticated user's profile.""" return render(request, "users/profile.html") diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..07636d0 --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,174 @@ +# StudyBuddy UI Design System + +Source inspiration: https://athenify.io/ + +This document is the canonical UI contract for StudyBuddy templates. The goal is not to copy Athenify verbatim. The goal is to give StudyBuddy the same category feel: a polished study-product website with a bright marketing shell, a blue brand accent, dark rounded calls to action, and dashboard visuals that make study tracking feel motivating. + +## Visual Direction + +StudyBuddy should feel like: + +- A student-focused study tracker and planner. +- Clean, white, spacious, and product-led. +- More like a refined landing/product app than a generic admin panel. +- Built around visual dashboard previews, compact metric cards, progress pills, streaks, and study-session controls. + +Avoid a plain left-sidebar SaaS layout for the public shell. Athenify's first viewport uses a top navigation bar, centered hero content, large product imagery, soft shadows, and very little background color. + +## Layout Model + +All templates must extend `/templates/base.html`. + +The base template provides: + +- White top navigation. +- Brand lockup on the left. +- Product/category navigation in the center. +- Authentication actions on the right. +- A constrained main content area. + +Page content follows: + +`Page shell -> Hero or Page Header -> Section -> Card -> Action` + +Use the dashboard-app visual language inside cards and previews, not as the global frame. + +## Color System + +Use CSS variables from `/static/css/theme.css`. + +Primary colors: + +| Token | Purpose | +| --- | --- | +| `--color-page` | Overall page background | +| `--color-surface` | Cards, nav, panels | +| `--color-ink` | Main text | +| `--color-muted` | Supporting text | +| `--color-line` | Borders | +| `--color-blue` | Brand blue | +| `--color-dark` | Dark CTA and app sidebar | +| `--color-green` | Progress and success indicators | + +Rules: + +- Use white as the dominant page color. +- Use blue mainly for brand and small highlights. +- Use dark navy/black for primary CTAs. +- Use green only for progress, streaks, and positive study metrics. +- Do not use purple-first palettes or generic Bootstrap button colors. + +## Typography + +Use the system UI stack. + +Hierarchy: + +| Element | Style | +| --- | --- | +| Brand | Bold, blue, compact | +| Hero headline | Large, black, tight line-height | +| Page title | Strong, dashboard-like | +| Section eyebrow | Small, uppercase, muted blue/grey | +| Section title | Bold, medium-large | +| Card title | Small, bold | +| Metadata | Small, muted | + +Hero copy should be direct and product-specific. Avoid generic filler. + +## Spacing + +Use the 8px scale only: + +| Token | Value | +| --- | --- | +| xs | 4px | +| sm | 8px | +| md | 16px | +| lg | 24px | +| xl | 32px | +| xxl | 48px | +| xxxl | 72px | + +Sections need generous vertical space. Cards stay compact and information dense. + +## Components + +### Header + +- White background. +- Logo/brand left. +- Horizontal nav. +- Right-aligned login and dark pill CTA. +- Sticky positioning is allowed when it does not obscure content. + +### Buttons + +- `btn-ui`: white button with soft border. +- `btn-ui-primary`: dark navy pill, used for primary action. +- `btn-ui-blue`: brand-blue secondary action. + +Buttons should be rounded pills, not rectangular admin buttons. + +### Cards + +- White surface. +- Soft grey border. +- 18-24px radius. +- Subtle shadow only for hero/product preview cards. +- Compact typography. + +### Product Mockups + +Athenify relies heavily on dashboard screenshots. StudyBuddy templates should use reusable HTML/CSS product mockups until real screenshots exist: + +- Browser/device frame. +- Dark app sidebar inside the mockup. +- Dashboard heading. +- Metric strip. +- Study streak row. +- Progress bars. +- Chart-like panels. + +This is a required visual pattern for public-facing pages. + +### Sections + +Every major section must have either: + +- An eyebrow plus title, or +- A clear section title. + +Sections should not be floating cards. Cards belong inside sections. + +## Template Rules + +Must: + +- Extend `base.html`. +- Use shared classes from `theme.css`. +- Use semantic HTML. +- Use card, section, button, metric, and mockup utilities. +- Keep all styling out of templates. + +Must not: + +- Reintroduce a global sidebar shell. +- Use inline styles. +- Use arbitrary spacing. +- Use Bootstrap visual classes as the main design system. +- Use placeholder admin UI that lacks product context. + +## Canonical Prompt + +When creating or refactoring StudyBuddy templates: + +Use Athenify.io as visual inspiration: white marketing shell, blue brand, dark rounded CTA, generous whitespace, large dashboard preview, compact metric cards, green progress indicators, and student-focused study tracking content. + +Follow: + +- `/docs/design-system.md` +- `/templates/base.html` +- `/static/css/theme.css` + +Output production-ready Django templates that extend the base template and use only shared design-system classes. diff --git a/static/css/theme.css b/static/css/theme.css new file mode 100644 index 0000000..d6782ef --- /dev/null +++ b/static/css/theme.css @@ -0,0 +1,923 @@ +/* File: /static/css/theme.css */ + +:root { + --color-page: #ffffff; + --color-soft: #f7f9fc; + --color-surface: #ffffff; + --color-ink: #0f172a; + --color-muted: #65738a; + --color-line: #e6eaf0; + --color-blue: #2557d6; + --color-blue-soft: #edf4ff; + --color-dark: #101827; + --color-dark-2: #1d2537; + --color-green: #18b76b; + --color-green-soft: #eafaf2; + --color-gold: #d99a22; + --shadow-soft: 0 20px 60px rgb(15 23 42 / 10%); + --shadow-card: 0 10px 30px rgb(15 23 42 / 6%); + + --radius-sm: 10px; + --radius-md: 16px; + --radius-lg: 24px; + --radius-pill: 999px; + + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-xxl: 48px; + --space-xxxl: 72px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--color-page); + color: var(--color-ink); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--color-blue); +} + +p { + color: var(--color-muted); + line-height: 1.6; +} + +.site-shell { + min-height: 100vh; + background: + radial-gradient(circle at 50% 120px, rgb(37 87 214 / 8%), transparent 280px), + linear-gradient(180deg, #fff 0%, #fff 62%, #f7f9fc 100%); +} + +.site-header { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid rgb(230 234 240 / 70%); + background: rgb(255 255 255 / 92%); + backdrop-filter: blur(18px); +} + +.site-nav { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-xl); + width: min(100%, 1240px); + min-height: 76px; + margin: 0 auto; + padding: 0 var(--space-xl); +} + +.brand-link { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--color-blue); + font-size: 1.35rem; + font-weight: 800; + line-height: 1; +} + +.brand-mark { + position: relative; + width: 18px; + height: 18px; + border-radius: 60% 42% 60% 42%; + background: var(--color-blue); + transform: rotate(-28deg); +} + +.brand-mark::after { + position: absolute; + right: 3px; + bottom: 3px; + width: 8px; + height: 2px; + border-radius: var(--radius-pill); + background: #ffffff; + content: ""; +} + +.nav-links, +.nav-actions, +.card-actions { + display: flex; + align-items: center; + gap: var(--space-md); +} + +.nav-links { + flex: 1; +} + +.nav-link-ui { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--color-ink); + font-weight: 700; + white-space: nowrap; +} + +.nav-link-ui::after { + width: 8px; + height: 8px; + border-right: 2px solid var(--color-blue); + border-bottom: 2px solid var(--color-blue); + margin-top: -4px; + content: ""; + transform: rotate(45deg); +} + +.nav-link-ui.active { + color: var(--color-blue); +} + +.site-main { + width: 100%; +} + +.container-ui { + width: min(100%, 1240px); + margin: 0 auto; + padding-right: var(--space-xl); + padding-left: var(--space-xl); +} + +.hero { + padding: var(--space-xxl) 0 var(--space-xxxl); + text-align: center; +} + +.hero-content { + display: grid; + justify-items: center; + gap: var(--space-lg); + max-width: 920px; + margin: 0 auto; + text-align: center; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + color: var(--color-blue); + font-size: 0.82rem; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + +.hero-title { + max-width: 850px; + margin: 0; + color: var(--color-ink); + font-size: clamp(2.75rem, 6vw, 5.75rem); + font-weight: 850; + letter-spacing: 0; + line-height: 0.96; +} + +.hero-title .text-blue { + color: var(--color-blue); +} + +.hero-subtitle { + max-width: 760px; + margin: 0; + color: var(--color-muted); + font-size: 1.14rem; +} + +.microcopy { + margin: 0; + color: var(--color-muted); + font-size: 0.92rem; +} + +.btn-ui { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 0 var(--space-lg); + border: 1px solid var(--color-line); + border-radius: var(--radius-pill); + background: var(--color-surface); + color: var(--color-ink); + font-weight: 800; + line-height: 1; + box-shadow: none; +} + +.btn-ui:hover { + border-color: #cfd7e3; + background: var(--color-soft); + color: var(--color-ink); +} + +.btn-ui-primary { + border-color: var(--color-dark); + background: var(--color-dark); + color: #ffffff; +} + +.btn-ui-primary:hover { + border-color: var(--color-dark-2); + background: var(--color-dark-2); + color: #ffffff; +} + +.btn-ui-blue { + border-color: var(--color-blue); + background: var(--color-blue); + color: #ffffff; +} + +.btn-ui-blue:hover { + border-color: #1d49bd; + background: #1d49bd; + color: #ffffff; +} + +.btn-ui-ghost { + border-color: transparent; + background: transparent; +} + +.section { + padding: var(--space-xxxl) 0; +} + +.section-tight { + padding: var(--space-xxl) 0; +} + +.section-header { + max-width: 760px; + margin-bottom: var(--space-xl); +} + +.section-title { + margin: var(--space-sm) 0 0; + color: var(--color-ink); + font-size: clamp(2rem, 3vw, 3rem); + font-weight: 850; + letter-spacing: 0; + line-height: 1.04; +} + +.section-copy { + margin: var(--space-md) 0 0; + font-size: 1.03rem; +} + +.page-stack { + display: grid; + gap: var(--space-xxl); + padding: var(--space-xxl) 0 var(--space-xxxl); +} + +.page-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-xl); +} + +.page-title { + margin: 0; + color: var(--color-ink); + font-size: clamp(2.25rem, 4vw, 4rem); + font-weight: 850; + letter-spacing: 0; + line-height: 1; +} + +.page-subtitle { + max-width: 720px; + margin: var(--space-md) 0 0; + font-size: 1.05rem; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-lg); +} + +.card-grid-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.card-ui { + border: 1px solid var(--color-line); + border-radius: var(--radius-lg); + background: var(--color-surface); + padding: var(--space-lg); + box-shadow: 0 1px 0 rgb(15 23 42 / 3%); +} + +.card-ui p:last-child { + margin-bottom: 0; +} + +.card-title-ui { + margin: 0 0 var(--space-sm); + color: var(--color-ink); + font-size: 1rem; + font-weight: 850; +} + +.feature-card { + min-height: 190px; +} + +.feature-icon { + display: grid; + place-items: center; + width: 42px; + height: 42px; + margin-bottom: var(--space-md); + border-radius: 14px; + background: var(--color-blue-soft); + color: var(--color-blue); + font-weight: 900; +} + +.metric-value { + display: block; + margin-bottom: var(--space-xs); + color: var(--color-ink); + font-size: 2rem; + font-weight: 850; + line-height: 1; +} + +.metric-label { + color: var(--color-muted); + font-size: 0.9rem; + font-weight: 700; +} + +.detail-list { + display: grid; + gap: var(--space-sm); + margin: 0; +} + +.detail-row { + display: flex; + justify-content: space-between; + gap: var(--space-lg); + padding: var(--space-md) 0; + border-bottom: 1px solid var(--color-line); +} + +.detail-row:last-child { + border-bottom: 0; +} + +.detail-row dt { + color: var(--color-muted); + font-weight: 700; +} + +.detail-row dd { + margin: 0; + color: var(--color-ink); + font-weight: 800; + text-align: right; +} + +.form-card { + max-width: 560px; + margin: 0 auto; +} + +.form-ui { + display: grid; + gap: var(--space-md); +} + +.form-ui p { + display: grid; + gap: var(--space-xs); + margin: 0; +} + +.form-ui label { + color: var(--color-ink); + font-weight: 800; +} + +.form-ui input, +.form-ui select, +.form-ui textarea { + width: 100%; + min-height: 46px; + border: 1px solid var(--color-line); + border-radius: var(--radius-md); + padding: 0 var(--space-md); + background: var(--color-surface); + color: var(--color-ink); +} + +.form-ui textarea { + min-height: 120px; + padding-top: var(--space-md); +} + +.form-ui input:focus, +.form-ui select:focus, +.form-ui textarea:focus { + border-color: var(--color-blue); + box-shadow: 0 0 0 4px rgb(37 87 214 / 12%); + outline: none; +} + +.form-ui .helptext, +.form-ui ul { + color: var(--color-muted); + font-size: 0.86rem; +} + +.form-ui ul { + margin: 0; + padding-left: var(--space-lg); +} + +.form-errors { + color: #b42318; + font-size: 0.88rem; + font-weight: 700; +} + +.form-errors ul { + margin: 0; + padding-left: var(--space-lg); +} + +.text-muted-ui { + color: var(--color-muted); +} + +.hero-visual { + position: relative; + width: min(100%, 1060px); + margin: var(--space-xxl) auto 0; +} + +.product-frame { + overflow: hidden; + border: 1px solid #dfe5ee; + border-radius: 22px; + background: #ffffff; + box-shadow: var(--shadow-soft); +} + +.browser-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 38px; + padding: 0 var(--space-md); + background: #34373d; + color: #cbd5e1; + font-size: 0.75rem; +} + +.browser-dots { + display: flex; + gap: 6px; +} + +.browser-dots span { + width: 10px; + height: 10px; + border-radius: 50%; + background: #f36b5f; +} + +.browser-dots span:nth-child(2) { + background: #f5c451; +} + +.browser-dots span:nth-child(3) { + background: #65c96f; +} + +.mock-app { + display: grid; + grid-template-columns: 174px 1fr; + min-height: 520px; + background: #f8fafc; +} + +.mock-sidebar { + padding: var(--space-lg) var(--space-md); + background: linear-gradient(180deg, #192133 0%, #101827 100%); + color: #ffffff; +} + +.mock-brand { + margin-bottom: var(--space-lg); + font-weight: 850; +} + +.mock-menu { + display: grid; + gap: var(--space-sm); +} + +.mock-menu span { + display: block; + padding: 9px 12px; + border-radius: 10px; + color: rgb(255 255 255 / 70%); + font-size: 0.82rem; + font-weight: 750; +} + +.mock-menu span.active { + background: #071026; + color: #ffffff; +} + +.mock-dashboard { + padding: var(--space-lg); +} + +.mock-topline { + display: flex; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.mock-date { + color: var(--color-muted); + font-size: 0.82rem; + font-weight: 800; +} + +.mock-dashboard h2 { + margin: 0; + color: var(--color-ink); + font-size: 2rem; + font-weight: 900; + line-height: 1; +} + +.mock-pill-row { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + margin-top: var(--space-sm); +} + +.mock-pill { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 12px; + border-radius: var(--radius-pill); + background: #f1f5f9; + color: var(--color-ink); + font-size: 0.78rem; + font-weight: 850; +} + +.mock-pill.dark { + background: var(--color-dark); + color: #ffffff; +} + +.mock-grid { + display: grid; + grid-template-columns: 1.08fr 1fr 1fr; + gap: var(--space-md); +} + +.mock-panel { + min-height: 170px; + border: 1px solid var(--color-line); + border-radius: 18px; + background: #ffffff; + padding: var(--space-md); + box-shadow: 0 10px 24px rgb(15 23 42 / 4%); +} + +.mock-panel.tall { + grid-row: span 2; + min-height: 356px; +} + +.mock-panel-title { + display: flex; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-md); + color: var(--color-ink); + font-size: 0.88rem; + font-weight: 900; +} + +.progress-line { + height: 8px; + overflow: hidden; + border-radius: var(--radius-pill); + background: #edf1f6; +} + +.progress-line span { + display: block; + width: 68%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #16a45f, #38d482); +} + +.card-ui:nth-child(2) .progress-line span, +.mock-panel:nth-child(3) .progress-line span { + width: 42%; +} + +.card-ui:nth-child(3) .progress-line span, +.mock-panel:nth-child(4) .progress-line span { + width: 26%; +} + +.streak-row { + display: flex; + justify-content: space-between; + gap: 7px; + margin: var(--space-md) 0; +} + +.streak-day { + display: grid; + place-items: center; + width: 32px; + height: 32px; + border: 3px solid #30c477; + border-radius: 50%; + color: #16a45f; + font-size: 0.78rem; + font-weight: 900; +} + +.chart-line { + position: relative; + height: 130px; + overflow: hidden; + border-radius: 16px; + background: + linear-gradient(180deg, rgb(37 129 245 / 18%), rgb(37 129 245 / 0%)), + linear-gradient(135deg, transparent 0 10%, rgb(37 129 245 / 18%) 10% 13%, transparent 13% 28%, rgb(37 129 245 / 25%) 28% 32%, transparent 32% 46%, rgb(37 129 245 / 32%) 46% 52%, transparent 52%); +} + +.chart-line::after { + position: absolute; + right: 8%; + bottom: 34%; + left: 7%; + height: 4px; + border-radius: var(--radius-pill); + background: var(--color-blue); + box-shadow: + 45px -18px 0 var(--color-blue), + 115px 20px 0 var(--color-blue), + 180px -22px 0 var(--color-blue), + 250px -8px 0 var(--color-blue); + content: ""; + transform: rotate(-8deg); +} + +.table-list { + display: grid; + gap: 12px; +} + +.table-row { + display: grid; + grid-template-columns: 1fr 76px; + gap: var(--space-md); + align-items: center; + color: var(--color-muted); + font-size: 0.82rem; + font-weight: 800; +} + +.donut { + width: 132px; + height: 132px; + margin: var(--space-md) auto 0; + border-radius: 50%; + background: conic-gradient(#0f7c3f 0 52%, #36b878 52% 76%, #91d9ae 76% 90%, #d7e7dd 90% 100%); + box-shadow: inset 0 0 0 34px #ffffff; +} + +.phone-frame { + position: absolute; + right: -10px; + bottom: -18px; + width: 210px; + border: 8px solid #111827; + border-radius: 36px; + background: #ffffff; + box-shadow: 0 22px 45px rgb(15 23 42 / 24%); +} + +.phone-content { + min-height: 340px; + padding: var(--space-md); +} + +.phone-notch { + width: 66px; + height: 18px; + margin: 0 auto var(--space-sm); + border-radius: 0 0 14px 14px; + background: #111827; +} + +.phone-content h3 { + margin: 0 0 var(--space-md); + font-size: 1.4rem; + font-weight: 900; +} + +.mini-bars { + display: grid; + gap: 8px; +} + +.mini-bars span { + display: block; + height: 8px; + border-radius: var(--radius-pill); + background: #e8eef5; +} + +.mini-bars span:nth-child(1) { + width: 78%; + background: var(--color-green); +} + +.mini-bars span:nth-child(2) { + width: 52%; +} + +.mini-bars span:nth-child(3) { + width: 68%; + background: #63c68f; +} + +.trust-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: var(--space-md); + margin-top: var(--space-xl); + color: var(--color-muted); + font-size: 0.94rem; + font-weight: 750; +} + +.logo-pill { + padding: 8px 14px; + border: 1px solid var(--color-line); + border-radius: var(--radius-pill); + background: #ffffff; +} + +.split-section { + display: grid; + grid-template-columns: 0.95fr 1.05fr; + gap: var(--space-xxl); + align-items: center; +} + +.quote-card { + display: grid; + gap: var(--space-md); + padding: var(--space-xl); + border-radius: var(--radius-lg); + background: var(--color-dark); + color: #ffffff; +} + +.quote-card p, +.quote-card .text-muted-ui { + color: rgb(255 255 255 / 76%); +} + +.quote-card .section-title { + color: #ffffff; +} + +.medal-row { + display: flex; + gap: var(--space-lg); + margin-top: var(--space-md); +} + +.medal { + color: var(--color-gold); + font-size: 1.7rem; + font-weight: 900; +} + +@media (max-width: 980px) { + .site-nav { + flex-wrap: wrap; + gap: var(--space-md); + padding: var(--space-md); + } + + .nav-links { + order: 3; + width: 100%; + overflow-x: auto; + padding-bottom: var(--space-xs); + } + + .card-grid, + .card-grid-2, + .split-section { + grid-template-columns: 1fr; + } + + .mock-app { + grid-template-columns: 1fr; + } + + .mock-sidebar { + display: none; + } + + .mock-grid { + grid-template-columns: 1fr; + } + + .phone-frame { + display: none; + } +} + +@media (max-width: 640px) { + .container-ui { + padding-right: var(--space-md); + padding-left: var(--space-md); + } + + .hero { + padding-top: var(--space-xl); + } + + .nav-actions { + width: 100%; + justify-content: stretch; + } + + .nav-actions .btn-ui { + flex: 1; + } + + .card-actions, + .page-header { + align-items: stretch; + flex-direction: column; + } + + .product-frame { + border-radius: 16px; + } + + .mock-dashboard { + padding: var(--space-md); + } + + .mock-topline { + flex-direction: column; + } +} diff --git a/templates/base.html b/templates/base.html index e827177..b9bc07e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,25 +1,50 @@ - - - - - +{% load static %} + + + + + + {% block title %}StudyBuddy{% endblock %} - - -
- -
-
- {% block content %}{% endblock %} -
- + + + + + + +
+ + +
+ {% block content %}{% endblock %} +
+
+ diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index d229ae0..3129180 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -3,5 +3,106 @@ {% block title %}Dashboard | StudyBuddy{% endblock %} {% block content %} -

Dashboard

+
+
+ + +
+
+
+ 0h + Today +
+
+
+ 0% + Weekly goal +
+
+
+ 0 + Sessions tracked +
+
+
+
+ +
+
+
+
StreaksStart today
+
+ MTWTF +
+
Study TrendThis week
+
+
MedalsComing soon
+
+ 0 + 0 + 0 +
+
+ +
+
GoalsDaily
+ 2h 0m + Suggested study goal +
+
+ +
+
Study Days0/365
+ 0% + Year progress +
+
+ +
+
CoursesNext
+
+
Create first course
+
Add a study goal
+
Track session
+
+
+ +
+
ActivitiesPlanned
+
+
+
+
+ +
+
+
+ 01 +

Build your first study plan

+

Add a subject, target study time, and the first session you want to complete.

+ +
+
+ 02 +

Review account settings

+

Confirm your StudyBuddy profile before personal planning preferences are added.

+ +
+
+
+
+
{% endblock %} diff --git a/templates/home.html b/templates/home.html index 8b7ff34..e7578de 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,7 +1,184 @@ {% extends "base.html" %} -{% block title %}StudyBuddy{% endblock %} +{% block title %}StudyBuddy | Study Tracker and Planner{% endblock %} {% block content %} -

StudyBuddy

+
+
+
+ The study tracker and planner for students +

Build momentum with a beautiful study dashboard

+

Track study sessions, plan goals, review streaks, and keep your coursework visible across one focused StudyBuddy workspace.

+ +

No credit card required. Works for courses, exams, and independent study.

+
+ +
+
+
+ + app.studybuddy.local + Dashboard +
+
+ +
+
+
+
06.05.26 - Week 19 - Studytracking
+

Dashboard

+
+ 12h 40m studied + 68% weekly goal + 14 day streak +
+
+ + Session +
+ +
+
+
Streaks14 days
+
+ MTWTF +
+
Share Price+2h 18m
+
+
MedalsThis month
+
+ 61 + 29 + 10 +
+
+ +
+
GoalsToday
+ 2h 0m + Daily study goal +
+
+ +
+
Study Days112/365
+ 31% + Year progress +
+
+ +
+
Courses6
+
+
Biology
+
Statistics
+
Writing
+
+
+ +
+
Activities15
+
+
+
+
+
+
+ + +
+ +
+ Built for focused students + Courses + Exams + Study Habits + Progress +
+
+
+ +
+
+
+ What is StudyBuddy? +

Study planning, tracking, and motivation in one place.

+

StudyBuddy gives students a clear picture of what they planned, what they completed, and where their next focused session should go.

+
+
+
+ 01 +

Track Sessions

+

Log study time and keep a visible record of steady work.

+
+
+ 02 +

Plan Goals

+

Turn courses, exams, and deadlines into achievable study targets.

+
+
+
+
+ +
+
+
+ Study Guides +

From scattered effort to visible progress.

+
+
+
+ T +

Focus Timer

+

Use session blocks to stay in the zone and avoid vague study days.

+
+
+ S +

Streaks

+

Build consistency with daily signals that make momentum obvious.

+
+
+ G +

Goal Progress

+

See how each course is moving against the study target you set.

+
+
+
+
+ +
+
+
+ Get started +

Start building your study dashboard today.

+

StudyBuddy is ready for account creation, authentication, and the first dashboard foundation. More planning features can build on this visual system.

+ +
+
+
{% endblock %} diff --git a/templates/registration/login.html b/templates/registration/login.html index fca178d..e1d5ba2 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -1,12 +1,29 @@ {% extends "base.html" %} -{% block title %}Log in | StudyBuddy{% endblock %} +{% block title %}Log In | StudyBuddy{% endblock %} {% block content %} -

Log in

-
- {% csrf_token %} - {{ form.as_p }} - -
+
+
+
+ Welcome back +

Log In

+

Return to your StudyBuddy dashboard and continue tracking study momentum.

+
+ +
+
+

Account Access

+
+ {% csrf_token %} + {{ form.as_p }} +
+ + Create Account +
+
+
+
+
+
{% endblock %} diff --git a/templates/registration/signup.html b/templates/registration/signup.html index b752749..20e0ecd 100644 --- a/templates/registration/signup.html +++ b/templates/registration/signup.html @@ -1,12 +1,29 @@ {% extends "base.html" %} -{% block title %}Sign up | StudyBuddy{% endblock %} +{% block title %}Create Account | StudyBuddy{% endblock %} {% block content %} -

Sign up

-
- {% csrf_token %} - {{ form.as_p }} - -
+
+
+
+ Start tracking +

Create Account

+

Create your StudyBuddy account and start building a dashboard for study sessions, goals, and progress.

+
+ +
+
+

New Account

+
+ {% csrf_token %} + {{ form.as_p }} +
+ + Log In +
+
+
+
+
+
{% endblock %} diff --git a/templates/users/profile.html b/templates/users/profile.html index a894908..eb28f9d 100644 --- a/templates/users/profile.html +++ b/templates/users/profile.html @@ -3,5 +3,61 @@ {% block title %}Profile | StudyBuddy{% endblock %} {% block content %} -

Profile

+
+
+ + +
+
+
+

Account Details

+
+
+
Display name
+
{{ request.user.display_name }}
+
+
+
Email
+
{{ request.user.email }}
+
+
+
Username
+
{{ request.user.username }}
+
+
+
+ +
+

Study Workspace

+

Personal scheduling, course targets, and notification preferences will appear here as StudyBuddy grows.

+
+ Study goals + Sessions + Progress +
+
+
+
+ +
+
+ Next step +

Make your study effort measurable.

+

Head back to the dashboard to start shaping the first trackable study workflow.

+ +
+
+
+
{% endblock %} diff --git a/templates/users/signup.html b/templates/users/signup.html new file mode 100644 index 0000000..d938b67 --- /dev/null +++ b/templates/users/signup.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block title %}Create Account | StudyBuddy{% endblock %} + +{% block content %} +
+
+
+ Start tracking +

Create Account

+

Create your StudyBuddy account and start building a dashboard for study sessions, goals, and progress.

+
+ +
+
+

New Account

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + + {% for field in form %} +

+ + {{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} + {% if field.errors %} + {{ field.errors }} + {% endif %} +

+ {% endfor %} + +
+ + Log In +
+
+
+
+
+
+{% endblock %}