diff --git a/static/css/v3/buttons.css b/static/css/v3/buttons.css index fa6d19ed5..689467e03 100644 --- a/static/css/v3/buttons.css +++ b/static/css/v3/buttons.css @@ -70,6 +70,7 @@ .btn-secondary { border-color: var(--color-stroke-strong); + background-color: var(--color-surface-weak); color: var(--color-text-primary); } diff --git a/static/css/v3/components.css b/static/css/v3/components.css index 4989719dc..585fe62d8 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -54,3 +54,5 @@ @import "./library-discovery-card.css"; @import "./help-card.css"; @import "./quick-start-card.css"; +@import "./mailing-list-activity-card.css"; +@import "./user-profile-bio-card.css"; diff --git a/static/css/v3/mailing-list-activity-card.css b/static/css/v3/mailing-list-activity-card.css new file mode 100644 index 000000000..c506220d3 --- /dev/null +++ b/static/css/v3/mailing-list-activity-card.css @@ -0,0 +1,25 @@ +.mailing-list-activity-card__mailing-items { + display: grid; + grid-template-columns: auto 1fr; + width: 100%; + gap: var(--space-large); +} + +.mailing-list-activity-card__date { + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: var(--letter-spacing-tight); + width: 80px; +} + +.mailing-list-activity-card__headline { + color: var(--color-text-primary); + font-family: var(--font-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-relaxed); + letter-spacing: var(--letter-spacing-tight); +} diff --git a/static/css/v3/post-card.css b/static/css/v3/post-card.css index e59209140..8b7aef295 100644 --- a/static/css/v3/post-card.css +++ b/static/css/v3/post-card.css @@ -44,6 +44,15 @@ letter-spacing: -0.12px; } +.post-card__summary { + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: var(--letter-spacing-tight); +} + .post-card__meta > :not(:first-child)::before { content: "•"; user-select: none; diff --git a/static/css/v3/user-profile-bio-card.css b/static/css/v3/user-profile-bio-card.css new file mode 100644 index 000000000..e7ba5b62f --- /dev/null +++ b/static/css/v3/user-profile-bio-card.css @@ -0,0 +1,17 @@ +.bio-card__contributor_role { + color: var(--color-text-primary); + font-family: var(--font-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-relaxed); + letter-spacing: var(--letter-spacing-tight); +} + +.bio-card__contributor_libraries { + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-relaxed); + letter-spacing: var(--letter-spacing-tight); +} diff --git a/static/css/v3/user-profile-page.css b/static/css/v3/user-profile-page.css new file mode 100644 index 000000000..0dfac7ead --- /dev/null +++ b/static/css/v3/user-profile-page.css @@ -0,0 +1,77 @@ +/* ──────── Top Level Container ──────── */ +.user-profile__container { + margin-top: var(--space-xl); + padding: 0 var(--space-large); +} + +/* ──────── Header row, including profile card and buttons ──────── */ + +.user-profile__profile-card-row { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: var(--space-large); + align-items: flex-end; +} + +.user-profile__button-group { + display: flex; + flex-direction: row; + gap: var(--space-s); + height: fit-content; + align-self: flex-end; + max-width: 100%; + flex-wrap: wrap; +} + +/* ──────── Two Row Card Display ──────── */ + +.user-profile__profile-content-area { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-large); + margin-bottom: var(--space-xl); +} + + +.user-profile__col { + display: flex; + flex-direction: column; + gap: var(--space-large); +} + +/* ──────── Override card width to fill columns ────────*/ + +.user-profile__profile-content-area .card, +.user-profile__profile-content-area .card-group { + max-width: 100%; +} + +@media (max-width: 767px) { + /* ──────── Stack header row on mobile ──────── */ + .user-profile__profile-card-row { + flex-direction: column; + gap: var(--space-xl); + align-items: flex-start; + } + + /* ──────── Collapse to one column on mobile ──────── */ + .user-profile__profile-content-area { + grid-template-columns: 1fr; + } + + /* Lift the column wrappers out of the layout so the cards become + direct grid children and `order` can interleave them in the + single-column cascade. */ + .user-profile__col { + display: contents; + } + + .user-profile__bio { order: 1; } + .user-profile__achievements { order: 2; } + .user-profile__badges { order: 3; } + .user-profile__github { order: 4; } + .user-profile__posts { order: 5; } + .user-profile__mailing-list { order: 6; } + .user-profile__connections { order: 7; } +} diff --git a/templates/includes/icon.html b/templates/includes/icon.html index 80709f8da..e2381cae2 100644 --- a/templates/includes/icon.html +++ b/templates/includes/icon.html @@ -109,6 +109,16 @@ {% elif icon_name == "flag" %} + {% elif icon_name == "pixel-github" %} + + {% elif icon_name == "pixel-computer" %} + + {% elif icon_name == "pixel-email" %} + + {% elif icon_name == "pixel-slack" %} + + {% elif icon_name == "pixel-pencil" %} + {% endif %} {% endif %} diff --git a/templates/v3/includes/_mailing_list_activity_card.html b/templates/v3/includes/_mailing_list_activity_card.html new file mode 100644 index 000000000..da704fe20 --- /dev/null +++ b/templates/v3/includes/_mailing_list_activity_card.html @@ -0,0 +1,24 @@ +{% comment %} + Card that displays the latest mailing list activity + Inputs: + title: str, required - text that appears bolded at the top of the card + mailing_list_items: list, required - list of dicts, each with the values (date, headline, url) for linking the mailing list items +{% endcomment %} +
+
+ {{ title }} +
+
+
+
+ {% for it in mailing_list_items %} + {{ it.date|date:"n/d/y" }} + {{ it.headline }} + {% endfor %} +
+
+
+
+ {% include 'v3/includes/_button.html' with style='primary' url=primary_button_url label='View all activity' only %} +
+
diff --git a/templates/v3/includes/_post_card.html b/templates/v3/includes/_post_card.html index df1b2fbad..a0993b23c 100644 --- a/templates/v3/includes/_post_card.html +++ b/templates/v3/includes/_post_card.html @@ -47,6 +47,9 @@

{{ item.title }}

{% endif %} + {% if item.summary %} +
{{item.summary}}
+ {% endif %} {% if item.author and item.author.name %} {% include "v3/includes/_user_profile.html" with author=item.author only %} {% endif %} diff --git a/templates/v3/includes/_user_profile_bio_card.html b/templates/v3/includes/_user_profile_bio_card.html new file mode 100644 index 000000000..3af43b626 --- /dev/null +++ b/templates/v3/includes/_user_profile_bio_card.html @@ -0,0 +1,34 @@ +{% load wagtailmarkdown %} +{% comment %} + Card for displaying the users bio data on their profile. Accepts markdown content and library contributor_data and displays it in a card layout. + Inputs: + title: str, required - text that appears bolded at the top of the card + contributor_data: dict, required - a dict of contributions and libraries (i.e. {"author": "beast", "asyncio"}) + markdown: str, required - markdown formatted text that appears in the body of the card. The card handles transforming the text + button_url: str, optional - url that the button should link to. If not provided, button is not shown + button_label: str, optional - label for the optional cta button. If not provided, button is not shown. +{% endcomment %} +
+
+ {{ title }} +
+
+ {% if contributor_data %} +
+ {% for role, libraries in contributor_data.items %} +
+
{{ role }}
+
{{ libraries|join:', ' }}
+
+ {% endfor %} +
+
+ {% endif %} +
{{ markdown|markdown }}
+ {% if button_url and button_label %} +
+
+ {% include 'v3/includes/_button.html' with url=button_url label=button_label only %} +
+ {% endif %} +
diff --git a/templates/v3/user_profile_page.html b/templates/v3/user_profile_page.html new file mode 100644 index 000000000..7c47f7733 --- /dev/null +++ b/templates/v3/user_profile_page.html @@ -0,0 +1,59 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %} + User Profile +{% endblock %} + +{% block extra_head %} + {{ block.super }} + +{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/users/views.py b/users/views.py index 09fc67c0e..e5eb2d5dd 100644 --- a/users/views.py +++ b/users/views.py @@ -1,4 +1,5 @@ import datetime +from textwrap import dedent from allauth.account import app_settings from django.contrib import messages @@ -23,6 +24,7 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from waffle import flag_is_active +from core.constants import BadgeToken from core.mixins import V3Mixin from libraries.models import CommitAuthorEmail from .forms import ( @@ -38,6 +40,9 @@ from . import tasks +from core.mock_data import SharedResources # noqa: F401 + + class UserViewSet(viewsets.ModelViewSet): """ Main User API ViewSet @@ -88,10 +93,230 @@ def get_context_data(self, **kwargs): return context -class CurrentUserProfileView(LoginRequiredMixin, SuccessMessageMixin, TemplateView): +class CurrentUserProfileView( + V3Mixin, LoginRequiredMixin, SuccessMessageMixin, TemplateView +): template_name = "users/profile.html" success_message = "Your profile was successfully updated." success_url = reverse_lazy("profile-account") + v3_template_name = "v3/user_profile_page.html" + + def get_v3_context_data(self, **kwargs): + user = self.request.user + ctx = {} + ctx["user_info"] = { + "user_name": user.display_name, + "avatar_url": user.get_avatar_url(), + "featured_badge": { + "name": "Bug Catcher", + "badge": BadgeToken.TIER_5, + }, + "member_since": user.date_joined.year, + "role": "Contributor", + } + + # Data shared between both versions, Boost Github and Mailing List activity + ctx["github_activity_card_data"] = { + "title": "Latest Boost Github activity", + "markdown_text": dedent( + """ + * Created 24 Commits in [7 repositories](https://www.example.com) + * Created [1 repository](https://www.example.com) + * Created a pull request in cppalliance/buffers that received [6 comments](https://www.example.com) + * Opened 17 other pull requests in [6 repositories](https://www.example.com) + * Reviewed 3 pull requests in [3 repositories](https://www.example.com) + """ + ), + "button_url": "https://www.github.com", + "button_label": "View on Github", + } + ctx["mailing_list_activity_card_data"] = { + "title": "Mailing List Activity", + "mailing_list_items": [ + { + "date": datetime.date(2025, 7, 11), + "headline": "[release] Boost 1.90.0 Beta 1 Release Candidate 1 is available", + "url": "#", + }, + { + "date": datetime.date(2025, 7, 11), + "headline": "[release] Boost 1.90.0 Beta 1 Release Candidate 1 is available", + "url": "#", + }, + { + "date": datetime.date(2025, 7, 11), + "headline": "[release] Boost 1.90.0 Beta 1 Release Candidate 1 is available", + "url": "#", + }, + { + "date": datetime.date(2025, 7, 11), + "headline": "[release] Boost 1.90.0 Beta 1 Release Candidate 1 is available", + "url": "#", + }, + { + "date": datetime.date(2025, 7, 11), + "headline": "[release] Boost 1.90.0 Beta 1 Release Candidate 1 is available", + "url": "#", + }, + ], + } + ctx["account_connections_none_connected"] = [ + { + "platform": "github", + "label": "GitHub", + "connected": False, + "status_text": "Not connected", + "action_label": "Connect", + "action_url": "#", + }, + { + "platform": "google", + "label": "Google", + "connected": False, + "status_text": "Not connected", + "action_label": "Connect", + "action_url": "#", + }, + ] + + if self.request.GET.get("filled"): + ctx["bio"] = dedent( + """ + **Professional Profile** + + I am a software engineer and C++ expert with extensive experience in systems programming and open-source software development. My work focuses on advancing the C++ ecosystem through libraries, tools, and community leadership. + + **Boost Library Author** + + I have authored and maintain several widely-used Boost libraries that are relied upon by developers worldwide. These libraries provide robust, production-ready components for modern C++ applications. + + **President of The C++ Alliance** + + As President of The C++ Alliance, I lead initiatives to support and advance the C++ programming language and its community. The Alliance provides resources, funding, and infrastructure to support C++ development, education, and standardization efforts. + + **Creator of Mr. Docs** + + I created Mr. Docs, a documentation generation tool designed specifically for C++ projects. Mr. Docs helps developers create high-quality, maintainable documentation that keeps pace with modern C++ codebases. + + **My primary technical interests include:** + + * HTTP Protocol: Implementation and optimization of HTTP client and server libraries + * WebSocket Protocol: Real-time bidirectional communication protocols and their practical applications + * Network Programming: High-performance asynchronous networking solutions in C++ + + These interests have shaped my contributions to the C++ ecosystem, particularly in developing libraries that make network programming more accessible and efficient for developers. + """ + ) + ctx["contributor_data"] = { + "Author": ["Beast", "JSON"], + "Maintainer": ["Beast", "Accumulator"], + "Contributor": [ + "Beast", + "JSON", + "Accumulator", + "Asio", + "Blood", + "Redis", + "MQTT5", + ], + "Reviews": ["Asio", "Blood (Manager)", "Redis", "MQTT5"], + } + ctx["profile_post_cta_label"] = "View All Posts" + ctx["profile_post_cta_url"] = "#" + ctx["achievements_data"] = { + "achievements": [ + { + "title": "Lorem Ipsum", + "points": 22, + "description": "A longer description giving a summary of the achievement.", + } + for _ in range(6) + ] + } + ctx["demo_badges"] = [ + { + "icon": BadgeToken.TIER_1, + "name": "Code Whisperer", + "earned_date": "01/01/2025", + }, + { + "icon": BadgeToken.TIER_2, + "name": "Library Alchemist", + "earned_date": "03/04/2025", + }, + { + "icon": BadgeToken.TIER_3, + "name": "Patch Wizard", + "earned_date": "08/08/2025", + }, + { + "icon": BadgeToken.TIER_4, + "name": "Bug Catcher", + "earned_date": "02/04/2025", + }, + { + "icon": BadgeToken.TIER_5, + "name": "Standard Bearer", + "earned_date": "03/07/2025", + }, + { + "icon": BadgeToken.STAR_TIER_3, + "name": "Review Hawk", + "earned_date": "03/06/2025", + }, + ] + ctx["posts"] = [ + { + "title": "A talk by Richard Thomson at the Utah C++ Programmers Group", + "url": "#", + "date": datetime.date(2025, 3, 3), + "category": "Issues", + "tag": "beast", + }, + { + "title": "A talk by Richard Thomson at the Utah C++ Programmers Group", + "url": "#", + "date": datetime.date(2025, 3, 3), + "category": "Issues", + "tag": "beast", + }, + { + "title": "Boost.Bind and modern C++: a quick overview", + "url": "#", + "date": datetime.date(2025, 2, 15), + "category": "Releases", + "tag": "bind", + }, + { + "title": "Boost.Bind and modern C++: a quick overview again", + "url": "#", + "date": datetime.date(2025, 2, 15), + "category": "Releases", + "tag": "bind", + }, + { + "title": "utility::string_view and core::detail::string_view", + "url": "#", + "date": datetime.date(2025, 2, 15), + "category": "Releases", + "tag": "bind", + }, + ] + + else: + ctx["posts"] = [ + { + "title": "Share Your Knowledge with the community", + "summary": "Write posts to share ideas, tutorials, announcements, or lessons learned about working with Boost", + } + ] + ctx["bio"] = ( + "Add a short bio to tell the community who you are, what you work on, or what you’re passionate about." + ) + ctx["profile_post_cta_label"] = "Create a Post" + ctx["profile_post_cta_url"] = "#" + + return ctx def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs)