diff --git a/accounts/views.py b/accounts/views.py index 3fb1fc5..b37162a 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import redirect, get_object_or_404, render from django.views.generic import ListView, CreateView, DeleteView, View, TemplateView, UpdateView from accounts.models import FriendCode, CustomUser from accounts.forms import FriendCodeForm, UserSettingsForm @@ -9,6 +9,7 @@ from django.db.models import Case, When, Value, BooleanField from trades.models import TradeOffer, TradeAcceptance from django.core.exceptions import PermissionDenied from trades.mixins import FriendCodeRequiredMixin +from common.mixins import ReusablePaginationMixin class ListFriendCodesView(LoginRequiredMixin, ListView): """ @@ -150,7 +151,7 @@ class EditFriendCodeView(LoginRequiredMixin, UpdateView): messages.success(self.request, "Friend code updated successfully.") return super().form_valid(form) -class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView): +class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, ReusablePaginationMixin, TemplateView): template_name = "account/dashboard.html" def post(self, request, *args, **kwargs): @@ -179,10 +180,10 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView): return selected_friend_code def get_dashboard_offers_paginated(self, page_param): - from django.core.paginator import Paginator selected_friend_code = self.get_selected_friend_code() queryset = TradeOffer.objects.filter(initiated_by=selected_friend_code, is_closed=False) - return Paginator(queryset, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(queryset, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_involved_acceptances(self, selected_friend_code): from django.db.models import Q @@ -209,8 +210,8 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView): ]) | Q(accepted_by=selected_friend_code, state__in=[TradeAcceptance.AcceptanceState.SENT]) ) - from django.core.paginator import Paginator - return Paginator(waiting, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(waiting, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_other_party_trade_acceptances_paginated(self, page_param): selected_friend_code = self.get_selected_friend_code() @@ -224,18 +225,17 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView): Q(accepted_by=selected_friend_code, state__in=[TradeAcceptance.AcceptanceState.SENT]) ) others = involved.exclude(pk__in=waiting.values("pk")) - from django.core.paginator import Paginator - return Paginator(others, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(others, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_closed_offers_paginated(self, page_param): - from django.core.paginator import Paginator selected_friend_code = self.get_selected_friend_code() queryset = TradeOffer.objects.filter(initiated_by=selected_friend_code, is_closed=True) - return Paginator(queryset, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(queryset, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_closed_acceptances_paginated(self, page_param): from django.db.models import Q - from django.core.paginator import Paginator selected_friend_code = self.get_selected_friend_code() terminal_success_states = [ TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR, @@ -246,41 +246,83 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView): Q(trade_offer__initiated_by=selected_friend_code) | Q(accepted_by=selected_friend_code), state__in=terminal_success_states ).order_by("-updated_at") - return Paginator(acceptance_qs, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(acceptance_qs, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_rejected_by_me_paginated(self, page_param): from django.db.models import Q - from django.core.paginator import Paginator selected_friend_code = self.get_selected_friend_code() rejection = TradeAcceptance.objects.filter( Q(trade_offer__initiated_by=selected_friend_code, state=TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR) | Q(accepted_by=selected_friend_code, state=TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR) ).order_by("-updated_at") - return Paginator(rejection, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(rejection, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_rejected_by_them_paginated(self, page_param): from django.db.models import Q - from django.core.paginator import Paginator selected_friend_code = self.get_selected_friend_code() rejection = TradeAcceptance.objects.filter( Q(trade_offer__initiated_by=selected_friend_code, state=TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR) | Q(accepted_by=selected_friend_code, state=TradeAcceptance.AcceptanceState.REJECTED_BY_INITIATOR) ).order_by("-updated_at") - return Paginator(rejection, 10).get_page(page_param) + object_list, pagination_context = self.paginate_data(rejection, int(page_param)) + return {"object_list": object_list, "page_obj": pagination_context} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) request = self.request selected_friend_code = self.get_selected_friend_code() context["selected_friend_code"] = selected_friend_code - context["friend_codes"] = request.user.friend_codes.all() - offers_page = request.GET.get("offers_page", 1) - waiting_page = request.GET.get("waiting_page", 1) - other_page = request.GET.get("other_page", 1) - closed_offers_page = request.GET.get("closed_offers_page", 1) - closed_acceptances_page = request.GET.get("closed_acceptances_page", 1) - rejected_by_me_page = request.GET.get("rejected_by_me_page", 1) - rejected_by_them_page = request.GET.get("rejected_by_them_page", 1) + + # Get the default friend code's primary key if it exists + default_pk = getattr(request.user.default_friend_code, "pk", None) + + # Annotate friend codes with is_default flag + context["friend_codes"] = request.user.friend_codes.all().annotate( + is_default=Case( + When(pk=default_pk, then=Value(True)), + default=Value(False), + output_field=BooleanField() + ) + ) + + ajax_section = request.GET.get("ajax_section") + if ajax_section == "dashboard_offers": + offers_page = request.GET.get("page", 1) + else: + offers_page = request.GET.get("offers_page", 1) + + if ajax_section == "waiting_acceptances": + waiting_page = request.GET.get("page", 1) + else: + waiting_page = request.GET.get("waiting_page", 1) + + if ajax_section == "other_party_acceptances": + other_page = request.GET.get("page", 1) + else: + other_page = request.GET.get("other_page", 1) + + if ajax_section == "closed_offers": + closed_offers_page = request.GET.get("page", 1) + else: + closed_offers_page = request.GET.get("closed_offers_page", 1) + + if ajax_section == "closed_acceptances": + closed_acceptances_page = request.GET.get("page", 1) + else: + closed_acceptances_page = request.GET.get("closed_acceptances_page", 1) + + if ajax_section == "rejected_by_me": + rejected_by_me_page = request.GET.get("page", 1) + else: + rejected_by_me_page = request.GET.get("rejected_by_me_page", 1) + + if ajax_section == "rejected_by_them": + rejected_by_them_page = request.GET.get("page", 1) + else: + rejected_by_them_page = request.GET.get("rejected_by_them_page", 1) + context["dashboard_offers_paginated"] = self.get_dashboard_offers_paginated(offers_page) context["trade_acceptances_waiting_paginated"] = self.get_trade_acceptances_waiting_paginated(waiting_page) context["other_party_trade_acceptances_paginated"] = self.get_other_party_trade_acceptances_paginated(other_page) @@ -291,4 +333,33 @@ class DashboardView(LoginRequiredMixin, FriendCodeRequiredMixin, TemplateView): from accounts.forms import UserSettingsForm context["settings_form"] = UserSettingsForm(instance=request.user) context["active_tab"] = request.GET.get("tab", "dash") - return context \ No newline at end of file + return context + + # Handle AJAX requests to return only the trade offer list fragment + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + ajax_section = request.GET.get("ajax_section") + if request.headers.get("X-Requested-With") == "XMLHttpRequest" and ajax_section: + if ajax_section == "dashboard_offers": + fragment_context = context.get("dashboard_offers_paginated", {}) + elif ajax_section == "waiting_acceptances": + fragment_context = context.get("trade_acceptances_waiting_paginated", {}) + elif ajax_section == "other_party_acceptances": + fragment_context = context.get("other_party_trade_acceptances_paginated", {}) + elif ajax_section == "closed_offers": + fragment_context = context.get("closed_offers_paginated", {}) + elif ajax_section == "closed_acceptances": + fragment_context = context.get("closed_acceptances_paginated", {}) + elif ajax_section == "rejected_by_me": + fragment_context = context.get("rejected_by_me_paginated", {}) + elif ajax_section == "rejected_by_them": + fragment_context = context.get("rejected_by_them_paginated", {}) + else: + fragment_context = {} + + if fragment_context: + return render(request, "trades/_trade_offer_list.html", { + "offers": fragment_context.get("object_list", []), + "page_obj": fragment_context.get("page_obj") + }) + return super().get(request, *args, **kwargs) \ No newline at end of file diff --git a/cards/views.py b/cards/views.py index 46e49fe..e98652f 100644 --- a/cards/views.py +++ b/cards/views.py @@ -3,7 +3,7 @@ from django.urls import reverse_lazy from django.views.generic import UpdateView, DeleteView, CreateView, ListView, DetailView from cards.models import Card from trades.models import TradeOffer -from cards.mixins import ReusablePaginationMixin +from common.mixins import ReusablePaginationMixin class CardDetailView(DetailView): model = Card @@ -65,7 +65,7 @@ class TradeOfferWantCardListView(ListView): class CardListView(ReusablePaginationMixin, ListView): model = Card - paginate_by = 36 # For non-grouped mode; grouping mode will override default pagination. + # Removed built-in pagination; using custom mixin instead context_object_name = "cards" def get_template_names(self): @@ -88,13 +88,6 @@ class CardListView(ReusablePaginationMixin, ListView): qs = qs.order_by(ordering) return qs.prefetch_related("decks").distinct() - def get_paginate_by(self, queryset): - group_by = self.request.GET.get("group_by") - if group_by in ("deck", "cardset", "rarity"): - # When grouping is enabled, we want to paginate manually so disable default pagination. - return None - return self.paginate_by - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) order = self.request.GET.get("order", "absolute") @@ -106,7 +99,6 @@ class CardListView(ReusablePaginationMixin, ListView): full_qs = self.get_queryset() all_cards = list(full_qs) flat_cards = [] - if group_by == "deck": for card in all_cards: for deck in card.decks.all(): @@ -121,16 +113,10 @@ class CardListView(ReusablePaginationMixin, ListView): flat_cards.append({"group": card.rarity_icon, "sort_group": card.rarity_level, "card": card}) flat_cards.sort(key=lambda x: x["sort_group"], reverse=True) - try: - page_number = int(self.request.GET.get("page", 1)) - except ValueError: - page_number = 1 - - # Use our custom mixin logic here + page_number = self.get_page_number() self.per_page = 36 page_flat_cards, pagination_context = self.paginate_data(flat_cards, page_number) - # Reassemble the flat list into groups for the current page. page_groups = [] for item in page_flat_cards: group_value = item["group"] @@ -143,19 +129,11 @@ class CardListView(ReusablePaginationMixin, ListView): context["page_obj"] = pagination_context context["total_cards"] = len(flat_cards) context["object_list"] = full_qs - return context else: - # For non-grouped mode, transform the built-in paginator page - if "page_obj" in context: - page = context["page_obj"] - # Create a unified pagination context dict with updated keys - custom_page_obj = { - "number": page.number, - "has_previous": page.has_previous(), - "has_next": page.has_next(), - "previous_page_number": page.previous_page_number() if page.has_previous() else 1, - "next_page_number": page.next_page_number() if page.has_next() else page.paginator.num_pages, - "paginator": {"num_pages": page.paginator.num_pages}, - } - context["page_obj"] = custom_page_obj - return context \ No newline at end of file + page_number = self.get_page_number() + self.per_page = 36 + paginated_cards, pagination_context = self.paginate_data(self.get_queryset(), page_number) + context["cards"] = paginated_cards + context["page_obj"] = pagination_context + context["object_list"] = self.get_queryset() + return context \ No newline at end of file diff --git a/common/mixins.py b/common/mixins.py new file mode 100644 index 0000000..6290fdc --- /dev/null +++ b/common/mixins.py @@ -0,0 +1,34 @@ +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger + + +class ReusablePaginationMixin: + per_page = 10 + + def get_page_number(self): + try: + return int(self.request.GET.get("page", 1)) + except (ValueError, TypeError): + return 1 + + def paginate_data(self, data, page_number): + """ + Paginates data (a QuerySet or list) and returns a tuple: (page_data, pagination_context). + """ + paginator = Paginator(data, self.per_page) + try: + page = paginator.page(page_number) + except PageNotAnInteger: + page = paginator.page(1) + except EmptyPage: + page = paginator.page(paginator.num_pages) + + pagination_context = { + "number": page.number, + "has_previous": page.has_previous(), + "has_next": page.has_next(), + "previous_page_number": page.previous_page_number() if page.has_previous() else 1, + "next_page_number": page.next_page_number() if page.has_next() else paginator.num_pages, + "paginator": {"num_pages": paginator.num_pages}, + "count": paginator.count + } + return page.object_list, pagination_context \ No newline at end of file diff --git a/static/css/base.css b/static/css/base.css index 6511b28..ce81a47 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -54,4 +54,40 @@ select.card-multiselect { } .choices__input { background-color: var(--color-base-100); +} + +.gravatar-hovercard .gravatar-hovercard__inner { + background-color: var(--color-base-100) !important; + border-color: var(--color-base-300) !important; + color: var(--color-base-content) !important; +} + +.gravatar-hovercard .gravatar-hovercard__inner, +.gravatar-hovercard .gravatar-hovercard__header-image, +.gravatar-hovercard .gravatar-hovercard__header, +.gravatar-hovercard .gravatar-hovercard__avatar-link, +.gravatar-hovercard .gravatar-hovercard__avatar, +.gravatar-hovercard .gravatar-hovercard__personal-info-plink, +.gravatar-hovercard .gravatar-hovercard__name, +.gravatar-hovercard .gravatar-hovercard__job, +.gravatar-hovercard .gravatar-hovercard__location, +.gravatar-hovercard .gravatar-hovercard__body, +.gravatar-hovercard .gravatar-hovercard__description, +.gravatar-hovercard .gravatar-hovercard__social-links, +.gravatar-hovercard .gravatar-hovercard__buttons, +.gravatar-hovercard .gravatar-hovercard__button, +.gravatar-hovercard .gravatar-hovercard__button:hover, +.gravatar-hovercard .gravatar-hovercard__footer, +.gravatar-hovercard .gravatar-hovercard__profile-url, +.gravatar-hovercard .gravatar-hovercard__profile-link, +.gravatar-hovercard .gravatar-hovercard__profile-color { + color: var(--color-base-content) !important; +} + +.gravatar-hovercard .gravatar-hovercard__location { + color: var(--color-base-content) !important; +} + +.dark .gravatar-hovercard .gravatar-hovercard__social-icon { + filter: invert(1) !important; } \ No newline at end of file diff --git a/static/css/choices.min.css b/static/css/choices.min.css new file mode 100644 index 0000000..cf79ea9 --- /dev/null +++ b/static/css/choices.min.css @@ -0,0 +1 @@ +.choices{position:relative;overflow:hidden;margin-bottom:24px;font-size:16px}.choices:focus{outline:0}.choices:last-child{margin-bottom:0}.choices.is-open{overflow:visible}.choices.is-disabled .choices__inner,.choices.is-disabled .choices__input{background-color:#eaeaea;cursor:not-allowed;-webkit-user-select:none;user-select:none}.choices.is-disabled .choices__item{cursor:not-allowed}.choices [hidden]{display:none!important}.choices[data-type*=select-one]{cursor:pointer}.choices[data-type*=select-one] .choices__inner{padding-bottom:7.5px}.choices[data-type*=select-one] .choices__input{display:block;width:100%;padding:10px;border-bottom:1px solid #ddd;background-color:#fff;margin:0}.choices[data-type*=select-one] .choices__button{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);padding:0;background-size:8px;position:absolute;top:50%;right:0;margin-top:-10px;margin-right:25px;height:20px;width:20px;border-radius:10em;opacity:.25}.choices[data-type*=select-one] .choices__button:focus,.choices[data-type*=select-one] .choices__button:hover{opacity:1}.choices[data-type*=select-one] .choices__button:focus{box-shadow:0 0 0 2px #005f75}.choices[data-type*=select-one] .choices__item[data-placeholder] .choices__button{display:none}.choices[data-type*=select-one]::after{content:"";height:0;width:0;border-style:solid;border-color:#333 transparent transparent;border-width:5px;position:absolute;right:11.5px;top:50%;margin-top:-2.5px;pointer-events:none}.choices[data-type*=select-one].is-open::after{border-color:transparent transparent #333;margin-top:-7.5px}.choices[data-type*=select-one][dir=rtl]::after{left:11.5px;right:auto}.choices[data-type*=select-one][dir=rtl] .choices__button{right:auto;left:0;margin-left:25px;margin-right:0}.choices[data-type*=select-multiple] .choices__inner,.choices[data-type*=text] .choices__inner{cursor:text}.choices[data-type*=select-multiple] .choices__button,.choices[data-type*=text] .choices__button{position:relative;display:inline-block;margin:0-4px 0 8px;padding-left:16px;border-left:1px solid #003642;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);background-size:8px;width:8px;line-height:1;opacity:.75;border-radius:0}.choices[data-type*=select-multiple] .choices__button:focus,.choices[data-type*=select-multiple] .choices__button:hover,.choices[data-type*=text] .choices__button:focus,.choices[data-type*=text] .choices__button:hover{opacity:1}.choices__inner{display:inline-block;vertical-align:top;width:100%;background-color:#f9f9f9;padding:7.5px 7.5px 3.75px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;min-height:44px;overflow:hidden}.is-focused .choices__inner,.is-open .choices__inner{border-color:#b7b7b7}.is-open .choices__inner{border-radius:2.5px 2.5px 0 0}.is-flipped.is-open .choices__inner{border-radius:0 0 2.5px 2.5px}.choices__list{margin:0;padding-left:0;list-style:none}.choices__list--single{display:inline-block;padding:4px 16px 4px 4px;width:100%}[dir=rtl] .choices__list--single{padding-right:4px;padding-left:16px}.choices__list--single .choices__item{width:100%}.choices__list--multiple{display:inline}.choices__list--multiple .choices__item{display:inline-block;vertical-align:middle;border-radius:20px;padding:4px 10px;font-size:12px;font-weight:500;margin-right:3.75px;margin-bottom:3.75px;background-color:#005f75;border:1px solid #004a5c;color:#fff;word-break:break-all;box-sizing:border-box}.choices__list--multiple .choices__item[data-deletable]{padding-right:5px}[dir=rtl] .choices__list--multiple .choices__item{margin-right:0;margin-left:3.75px}.choices__list--multiple .choices__item.is-highlighted{background-color:#004a5c;border:1px solid #003642}.is-disabled .choices__list--multiple .choices__item{background-color:#aaa;border:1px solid #919191}.choices__list--dropdown,.choices__list[aria-expanded]{display:none;z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;border-bottom-left-radius:2.5px;border-bottom-right-radius:2.5px;overflow:hidden;word-break:break-all}.is-active.choices__list--dropdown,.is-active.choices__list[aria-expanded]{display:block}.is-open .choices__list--dropdown,.is-open .choices__list[aria-expanded]{border-color:#b7b7b7}.is-flipped .choices__list--dropdown,.is-flipped .choices__list[aria-expanded]{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-radius:.25rem .25rem 0 0}.choices__list--dropdown .choices__list,.choices__list[aria-expanded] .choices__list{position:relative;max-height:300px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:scroll-position}.choices__list--dropdown .choices__item,.choices__list[aria-expanded] .choices__item{position:relative;padding:10px;font-size:14px}[dir=rtl] .choices__list--dropdown .choices__item,[dir=rtl] .choices__list[aria-expanded] .choices__item{text-align:right}@media (min-width:640px){.choices__list--dropdown .choices__item--selectable[data-select-text],.choices__list[aria-expanded] .choices__item--selectable[data-select-text]{padding-right:100px}.choices__list--dropdown .choices__item--selectable[data-select-text]::after,.choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after{content:attr(data-select-text);font-size:12px;opacity:0;position:absolute;right:10px;top:50%;transform:translateY(-50%)}[dir=rtl] .choices__list--dropdown .choices__item--selectable[data-select-text],[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable[data-select-text]{text-align:right;padding-left:100px;padding-right:10px}[dir=rtl] .choices__list--dropdown .choices__item--selectable[data-select-text]::after,[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after{right:auto;left:10px}}.choices__list--dropdown .choices__item--selectable.is-highlighted,.choices__list[aria-expanded] .choices__item--selectable.is-highlighted{background-color:#f2f2f2}.choices__list--dropdown .choices__item--selectable.is-highlighted::after,.choices__list[aria-expanded] .choices__item--selectable.is-highlighted::after{opacity:.5}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;user-select:none;opacity:.5}.choices__heading{font-weight:600;font-size:12px;padding:10px;border-bottom:1px solid #f7f7f7;color:gray}.choices__button{text-indent:-9999px;appearance:none;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.choices__button:focus,.choices__input:focus{outline:0}.choices__input{display:inline-block;vertical-align:baseline;background-color:#f9f9f9;font-size:14px;margin-bottom:5px;border:0;border-radius:0;max-width:100%;padding:4px 0 4px 2px}.choices__input::-webkit-search-cancel-button,.choices__input::-webkit-search-decoration,.choices__input::-webkit-search-results-button,.choices__input::-webkit-search-results-decoration{display:none}.choices__input::-ms-clear,.choices__input::-ms-reveal{display:none;width:0;height:0}[dir=rtl] .choices__input{padding-right:2px;padding-left:0}.choices__placeholder{opacity:.5} \ No newline at end of file diff --git a/static/css/hovercards.min.css b/static/css/hovercards.min.css new file mode 100644 index 0000000..37bc2a4 --- /dev/null +++ b/static/css/hovercards.min.css @@ -0,0 +1,3 @@ +.gravatar-hovercard{display:inline-block;z-index:10000000}.gravatar-hovercard h4,.gravatar-hovercard p{margin:0}.gravatar-hovercard p,.gravatar-hovercard i,.gravatar-hovercard a,.gravatar-hovercard span{font-family:"SF Pro Text",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:14px;line-height:1.5;color:#000}.gravatar-hovercard .gravatar-hovercard__inner{display:flex;flex-direction:column;justify-content:space-between;width:336px;min-height:273px;position:relative;padding:24px 24px 16px;background-color:#fff;border:1px solid #d8dbdd;border-radius:4px;box-shadow:0 2px 6px rgba(0,0,0,.08);box-sizing:border-box;overflow:hidden}.gravatar-hovercard .gravatar-hovercard__header-image{position:absolute;top:0;left:50%;height:75px;width:100%;transform:translateX(-50%)}.gravatar-hovercard .gravatar-hovercard__header{z-index:1;display:flex;flex-direction:column;gap:8px}.gravatar-hovercard .gravatar-hovercard__avatar-link,.gravatar-hovercard .gravatar-hovercard__social-link{display:inline-flex}.gravatar-hovercard .gravatar-hovercard__avatar{border-radius:50%;background-color:#eee}.gravatar-hovercard .gravatar-hovercard__personal-info-link{text-decoration:none}.gravatar-hovercard .gravatar-hovercard__name{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;word-break:break-word;font-family:"SF Pro Text",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:32px;font-weight:700;line-height:38px;color:#000}.gravatar-hovercard .gravatar-hovercard__job,.gravatar-hovercard .gravatar-hovercard__location{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;word-break:break-word;color:#707070}.gravatar-hovercard .gravatar-hovercard__body{min-height:42px;margin-top:8px}.gravatar-hovercard .gravatar-hovercard__description{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;word-break:break-word}.gravatar-hovercard .gravatar-hovercard__social-links{display:flex;align-items:center;gap:4px;margin-top:16px}.gravatar-hovercard .gravatar-hovercard__buttons{display:flex;gap:16px;margin-top:16px}.gravatar-hovercard .gravatar-hovercard__button{width:100%;min-height:42px;padding:8px;background:unset;border-radius:4px;border:1px solid rgba(29,79,196,.3);font-family:"SF Pro Text",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:15px;font-weight:600;line-height:21px;color:#1d4fc4;cursor:pointer}.gravatar-hovercard .gravatar-hovercard__button:hover{border:1px solid rgba(29,79,196,.6)}.gravatar-hovercard .gravatar-hovercard__footer{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:12px}.gravatar-hovercard .gravatar-hovercard__profile-url{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;word-break:break-word;word-break:break-all;color:#707070;text-decoration:none}.gravatar-hovercard .gravatar-hovercard__profile-link{text-decoration:none;flex-shrink:0;color:#707070}.gravatar-hovercard .gravatar-hovercard__profile-color{position:absolute;bottom:0;left:0;width:100%;height:4px}.gravatar-hovercard--skeleton .gravatar-hovercard__avatar-link,.gravatar-hovercard--skeleton .gravatar-hovercard__personal-info-link,.gravatar-hovercard--skeleton .gravatar-hovercard__social-link,.gravatar-hovercard--skeleton .gravatar-hovercard__profile-link,.gravatar-hovercard--skeleton .gravatar-hovercard__profile-url{background-color:#eee}.gravatar-hovercard--skeleton .gravatar-hovercard__avatar-link{width:104px;height:104px;border-radius:50%}.gravatar-hovercard--skeleton .gravatar-hovercard__personal-info-link{height:38px;width:70%}.gravatar-hovercard--skeleton .gravatar-hovercard__social-link{width:32px;height:32px;border-radius:50%}.gravatar-hovercard--skeleton .gravatar-hovercard__profile-url{width:50%;height:21px}.gravatar-hovercard--skeleton .gravatar-hovercard__profile-link{height:21px;width:96px}.gravatar-hovercard--error .gravatar-hovercard__inner{align-items:center;justify-content:center;gap:34px}.gravatar-hovercard--error .gravatar-hovercard__error-message{color:#707070}.gravatar-hovercard__drawer{position:absolute;top:0;left:0;bottom:0;right:0;visibility:hidden;overflow:hidden;z-index:1}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-backdrop{position:absolute;top:0;left:0;bottom:0;right:0;background-color:rgba(0,0,0,.4);opacity:0;transition:opacity .3s ease-in-out}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-card{position:absolute;left:0;bottom:0;width:100%;max-height:100%;display:flex;flex-direction:column;background-color:#fff;border-top-left-radius:4px;border-top-right-radius:4px;padding:20px 0;box-sizing:border-box;transform:translate3d(0, 100%, 0);transition:transform .3s ease-in-out}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-header{display:flex;align-items:center;justify-content:space-between;padding:0 20px;margin-bottom:16px}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-title{font-family:"SF Pro Text",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:18px;font-weight:700;line-height:27px;margin:0}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-close{display:flex;align-items:center;justify-content:center;border:none;background:none;padding:0;cursor:pointer}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-items{display:flex;flex-direction:column;list-style:none;margin:0;padding:0 20px;gap:12px;overflow:auto}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-item{display:flex;gap:8px}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-item-info{display:flex;flex-direction:column}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-item-label{font-weight:600;line-height:24px;text-transform:capitalize}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-item-link{color:#1d4fc4;text-decoration:none}.gravatar-hovercard__drawer .gravatar-hovercard__drawer-item-link:hover{text-decoration:underline}.gravatar-hovercard__drawer--open{visibility:visible}.gravatar-hovercard__drawer--open .gravatar-hovercard__drawer-backdrop{opacity:1}.gravatar-hovercard__drawer--open .gravatar-hovercard__drawer-card{transform:translate3d(0, 0, 0)}.gravatar-hovercard__drawer--closing{visibility:visible}.gravatar-hovercard__drawer--closing .gravatar-hovercard__drawer-backdrop{opacity:0}.gravatar-hovercard__drawer--closing .gravatar-hovercard__drawer-card{transform:translate3d(0, 100%, 0)} + +/*# sourceMappingURL=style.css.map*/ \ No newline at end of file diff --git a/static/js/alpinejs.collapse@3.14.8.min.js b/static/js/alpinejs.collapse@3.14.8.min.js new file mode 100644 index 0000000..a865343 --- /dev/null +++ b/static/js/alpinejs.collapse@3.14.8.min.js @@ -0,0 +1 @@ +(()=>{function g(n){n.directive("collapse",e),e.inline=(t,{modifiers:i})=>{i.includes("min")&&(t._x_doShow=()=>{},t._x_doHide=()=>{})};function e(t,{modifiers:i}){let r=l(i,"duration",250)/1e3,h=l(i,"min",0),u=!i.includes("min");t._x_isShown||(t.style.height=`${h}px`),!t._x_isShown&&u&&(t.hidden=!0),t._x_isShown||(t.style.overflow="hidden");let c=(d,s)=>{let o=n.setStyles(d,s);return s.height?()=>{}:o},f={transitionProperty:"height",transitionDuration:`${r}s`,transitionTimingFunction:"cubic-bezier(0.4, 0.0, 0.2, 1)"};t._x_transition={in(d=()=>{},s=()=>{}){u&&(t.hidden=!1),u&&(t.style.display=null);let o=t.getBoundingClientRect().height;t.style.height="auto";let a=t.getBoundingClientRect().height;o===a&&(o=h),n.transition(t,n.setStyles,{during:f,start:{height:o+"px"},end:{height:a+"px"}},()=>t._x_isShown=!0,()=>{Math.abs(t.getBoundingClientRect().height-a)<1&&(t.style.overflow=null)})},out(d=()=>{},s=()=>{}){let o=t.getBoundingClientRect().height;n.transition(t,c,{during:f,start:{height:o+"px"},end:{height:h+"px"}},()=>t.style.overflow="hidden",()=>{t._x_isShown=!1,t.style.height==`${h}px`&&u&&(t.style.display="none",t.hidden=!0)})}}}}function l(n,e,t){if(n.indexOf(e)===-1)return t;let i=n[n.indexOf(e)+1];if(!i)return t;if(e==="duration"){let r=i.match(/([0-9]+)ms/);if(r)return r[1]}if(e==="min"){let r=i.match(/([0-9]+)px/);if(r)return r[1]}return i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(g)});})(); diff --git a/static/js/alpinejs@3.14.8.min.js b/static/js/alpinejs@3.14.8.min.js new file mode 100644 index 0000000..a3be81c --- /dev/null +++ b/static/js/alpinejs@3.14.8.min.js @@ -0,0 +1,5 @@ +(()=>{var nt=!1,it=!1,W=[],ot=-1;function Ut(e){Rn(e)}function Rn(e){W.includes(e)||W.push(e),Mn()}function Wt(e){let t=W.indexOf(e);t!==-1&&t>ot&&W.splice(t,1)}function Mn(){!it&&!nt&&(nt=!0,queueMicrotask(Nn))}function Nn(){nt=!1,it=!0;for(let e=0;ee.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>$(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function te(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Ae(e){Xt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ue(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){kn(),ut.disconnect(),ft=!1}var le=[];function kn(){let e=ut.takeRecords();le.push(()=>e.length>0&&mt(e));let t=le.length;queueMicrotask(()=>{if(le.length===t)for(;le.length>0;)le.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return ue(),t}var pt=!1,Se=[];function rr(){pt=!0}function nr(){pt=!1,mt(Se),Se=[]}function mt(e){if(pt){Se=Se.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Zt.forEach(s=>s(o));for(let o of t)o.isConnected&&Qt.forEach(s=>s(o));t=null,r=null,n=null,i=null}function Ce(e){return z(B(e))}function k(e,t,r){return e._x_dataStack=[t,...B(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function B(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?B(e.host):e.parentNode?B(e.parentNode):[]}function z(e){return new Proxy({objects:e},Dn)}var Dn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Pn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Pn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>In(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function In(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function fe(e,t){let r=Ln(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Ln(e){let[t,r]=_t(e),n={interceptor:Re,...t};return te(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){re(i,e,t)}}function re(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} + +${r?'Expression: "'+r+`" + +`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function ke(e){let t=Me;Me=!1;let r=e();return Me=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return sr(...e)}var sr=xt;function ar(e){sr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...B(e)],i=typeof t=="function"?$n(n,t):Fn(n,t,e);return or.bind(null,e,t,i)}function $n(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(z([n,...e]),i);Ne(r,o)}}var gt={};function jn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return re(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Fn(e,t,r){let n=jn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=z([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>re(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>re(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>re(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var wt="x-";function C(e=""){return wt+e}function cr(e){wt=e}var De={};function d(e,t){return De[e]=t,{before(r){if(!De[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function lr(e){return Object.keys(De).includes(e)}function pe(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(dr((o,s)=>n[o]=s)).filter(mr).map(zn(n,r)).sort(Kn).map(o=>Bn(e,o))}function Et(e){return Array.from(e).map(dr()).filter(t=>!mr(t))}var yt=!1,de=new Map,ur=Symbol();function fr(e){yt=!0;let t=Symbol();ur=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:K,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Bn(e,t){let r=()=>{},n=De[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?de.get(ur).push(n):n())};return s.runCleanups=o,s}var Pe=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ie=e=>e;function dr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=pr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var pr=[];function ne(e){pr.push(e)}function mr({name:e}){return hr().test(e)}var hr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function zn(e,t){return({name:r,value:n})=>{let i=r.match(hr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Kn(e,t){let r=G.indexOf(e.type)===-1?bt:e.type,n=G.indexOf(t.type)===-1?bt:t.type;return G.indexOf(r)-G.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function D(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>D(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)D(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var _r=!1;function gr(){_r&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),_r=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` + */ + +const { computePosition, offset, flip, shift, arrow } = FloatingUIDOM; + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[data-tooltip-html]').forEach((el) => { + let tooltipContainer = null; + let arrowElement = null; + let fadeOutTimeout; + + const showTooltip = () => { + if (tooltipContainer) return; // Tooltip already visible + + // Retrieve the custom HTML content from the data attribute + const tooltipContent = el.getAttribute('data-tooltip-html'); + + // Create a container for the tooltip (with modern styling) + tooltipContainer = document.createElement('div'); + tooltipContainer.classList.add( + 'bg-black', 'text-white', + 'shadow-lg', 'rounded-lg', 'p-2', + // Transition classes for simple fade in/out + 'transition-opacity', 'duration-200', 'opacity-0' + ); + tooltipContainer.style.position = 'absolute'; + tooltipContainer.style.zIndex = '9999'; + + // Set the HTML content for the tooltip + tooltipContainer.innerHTML = '
' + tooltipContent + '
'; + + // Create the arrow element. The arrow is styled as a small rotated square. + arrowElement = document.createElement('div'); + arrowElement.classList.add( + 'w-3', 'h-3', + 'bg-black', + 'transform', 'rotate-45' + ); + arrowElement.style.position = 'absolute'; + + // Append the arrow into the tooltip container + tooltipContainer.appendChild(arrowElement); + + // Append the tooltip container to the document body + document.body.appendChild(tooltipContainer); + + // Use Floating UI to position the tooltip, including the arrow middleware + computePosition(el, tooltipContainer, { + middleware: [ + offset(8), + flip(), + shift({ padding: 5 }), + arrow({ element: arrowElement }) + ] + }).then(({ x, y, placement, middlewareData }) => { + Object.assign(tooltipContainer.style, { + left: `${x}px`, + top: `${y}px` + }); + + // Position the arrow using the arrow middleware data + const { x: arrowX, y: arrowY } = middlewareData.arrow || {}; + + // Reset any previous inline values + arrowElement.style.left = ''; + arrowElement.style.top = ''; + arrowElement.style.right = ''; + arrowElement.style.bottom = ''; + + // Adjust the arrow's position according to the placement + if (placement.startsWith('top')) { + arrowElement.style.bottom = '-4px'; + arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%'; + } else if (placement.startsWith('bottom')) { + arrowElement.style.top = '-4px'; + arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%'; + } else if (placement.startsWith('left')) { + arrowElement.style.right = '-4px'; + arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%'; + } else if (placement.startsWith('right')) { + arrowElement.style.left = '-4px'; + arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%'; + } + }); + + // Trigger a fade-in by moving from opacity-0 to opacity-100 + requestAnimationFrame(() => { + tooltipContainer.classList.remove('opacity-0'); + tooltipContainer.classList.add('opacity-100'); + }); + }; + + const hideTooltip = () => { + if (tooltipContainer) { + tooltipContainer.classList.remove('opacity-100'); + tooltipContainer.classList.add('opacity-0'); + // Remove the tooltip from the DOM after the transition duration + fadeOutTimeout = setTimeout(() => { + if (tooltipContainer && tooltipContainer.parentNode) { + tooltipContainer.parentNode.removeChild(tooltipContainer); + } + tooltipContainer = null; + arrowElement = null; + }, 200); // Matches the duration-200 class (200ms) + } + }; + + // Attach event listeners to show/hide the tooltip + el.addEventListener('mouseenter', showTooltip); + el.addEventListener('mouseleave', hideTooltip); + el.addEventListener('focus', showTooltip); + el.addEventListener('blur', hideTooltip); + }); +}); \ No newline at end of file diff --git a/staticfiles/js/tooltip.js.gz b/staticfiles/js/tooltip.js.gz new file mode 100644 index 0000000..d135c59 Binary files /dev/null and b/staticfiles/js/tooltip.js.gz differ diff --git a/theme/templates/account/dashboard.html b/theme/templates/account/dashboard.html index f129dd3..cd8763d 100644 --- a/theme/templates/account/dashboard.html +++ b/theme/templates/account/dashboard.html @@ -9,11 +9,12 @@
- - - + + + +
@@ -21,42 +22,44 @@
-
-
-

{{ _('Account Summary') }}

-

{{ _('Username:') }} {{ request.user.username }}

-

{{ _('Default Friend Code:') }} {{ selected_friend_code.friend_code }}

-

{{ _('Reputation Score:') }} {{ request.user.reputation_score }}

-
-

{{ _('Trade Summary') }}

-
-
-
{{ _('Your Trade Offers') }}
-
{{ dashboard_offers_paginated.paginator.count }}
-
{{ _('Active Offers') }}
+
+
+
+
{{ _('Your Reputation') }}
+
{{ request.user.reputation_score }}
+
{{ _('Current Score') }}
+
+
+
{{ _('Your Trade Offers') }}
+
{{ dashboard_offers_paginated.page_obj.count }}
+
{{ _('Active Offers') }}
+
-
-
{{ _('Waiting on You') }}
-
{{ trade_acceptances_waiting_paginated.paginator.count }}
-
{{ _('Pending Requests') }}
-
-
-
{{ _('Waiting on Them') }}
-
{{ other_party_trade_acceptances_paginated.paginator.count }}
-
{{ _('Pending Responses') }}
+
+
+
{{ _('Waiting on You') }}
+
{{ trade_acceptances_waiting_paginated.page_obj.count }}
+
{{ _('Pending Requests') }}
+
+
+
{{ _('Waiting on Them') }}
+
{{ other_party_trade_acceptances_paginated.page_obj.count }}
+
{{ _('Pending Responses') }}
+
-
+
@@ -64,36 +67,36 @@
- {% include 'trades/_trade_offer_list.html' with offers=dashboard_offers_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=dashboard_offers_paginated.object_list page_obj=dashboard_offers_paginated.page_obj %}
- {% include 'trades/_trade_offer_list.html' with offers=trade_acceptances_waiting_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=trade_acceptances_waiting_paginated.object_list page_obj=trade_acceptances_waiting_paginated.page_obj %}
- {% include 'trades/_trade_offer_list.html' with offers=other_party_trade_acceptances_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=other_party_trade_acceptances_paginated.object_list page_obj=other_party_trade_acceptances_paginated.page_obj %}
-
{{ _('Closed Offers') }} ({{ closed_offers_paginated.paginator.count }})
+
{{ _('Closed Offers') }} ({{ closed_offers_paginated.page_obj.count }})
- {% include 'trades/_trade_offer_list.html' with offers=closed_offers_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=closed_offers_paginated.object_list page_obj=closed_offers_paginated.page_obj %}
-
{{ _('Closed Acceptances') }} ({{ closed_acceptances_paginated.paginator.count }})
+
{{ _('Closed Acceptances') }} ({{ closed_acceptances_paginated.page_obj.count }})
- {% include 'trades/_trade_offer_list.html' with offers=closed_acceptances_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=closed_acceptances_paginated.object_list page_obj=closed_acceptances_paginated.page_obj %}
-
{{ _('Rejected by Them') }} ({{ rejected_by_them_paginated.paginator.count }})
+
{{ _('Rejected by Them') }} ({{ rejected_by_them_paginated.page_obj.count }})
- {% include 'trades/_trade_offer_list.html' with offers=rejected_by_them_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=rejected_by_them_paginated.object_list page_obj=rejected_by_them_paginated.page_obj %}
-
{{ _('Rejected by Me') }} ({{ rejected_by_me_paginated.paginator.count }})
+
{{ _('Rejected by Me') }} ({{ rejected_by_me_paginated.page_obj.count }})
- {% include 'trades/_trade_offer_list.html' with offers=rejected_by_me_paginated %} + {% include 'trades/_trade_offer_list.html' with offers=rejected_by_me_paginated.object_list page_obj=rejected_by_me_paginated.page_obj %}
@@ -105,21 +108,69 @@
{{ gravatar_profile.displayName|default:request.user.username }}

{{ gravatar_profile.displayName|default:request.user.username }}

- {{ _('View Gravatar Profile') }} + + Edit Profile on Gravatar + + + + +
{% else %}

{{ _('No Gravatar profile data available.') }}

{% endif %} {% endwith %}
-

{{ _('What is Gravatar?') }}

-

{{ _('Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites use Gravatar to display your avatar automatically.') }}

-

{{ _('How does it work?') }}

-

{{ _('If you have set up a Gravatar, your profile picture will appear whenever you use your email on supported sites. Updates made on Gravatar will reflect here.') }}

-

{{ _('Is it safe? What about privacy?') }}

-

{{ _('Gravatar is optional, and your email is hashed to maintain privacy. Your personal data remains secure.') }}

-

{{ _('Want to update or add a Gravatar?') }}

-

{{ _('Go to Gravatar.com to set up or change your avatar or profile. Your changes will appear here once saved!') }}

+

What is Gravatar?

+

Gravatar (Globally Recognized Avatar) is a free service that links your email address to a profile picture and, optionally, a profile. Many websites, including this one, use Gravatar to display your avatar and profile automatically.

+ +

How does it work?

+

If you've set up a Gravatar, your profile picture will appear here whenever you use your email on supported sites. When someone hovers over or clicks on your avatar, your Gravatar profile will also appear if you have one. If you don't have a Gravatar yet, you'll see a default image instead.

+ +

Is it safe? What about privacy?

+

Gravatar is completely optional, opt-in, and prioritizes your security and privacy. Your email is never shared and only a hashed version is sent to Gravatar, protecting your identity while ensuring that your email address is not exposed to bots or scrapers. Your personal data remains secure, and you maintain full control over your public profile.

+ +

Want to update or add a Gravatar?

+

Go to Gravatar.com to set up or change your avatar or profile. Your updates will appear here once saved!

+ +
+
+ + +
+
+ {% if friend_codes %} +
    + {% for code in friend_codes %} +
  • +
    + {{ code.in_game_name }} + {% if code.is_default %} + Default + {% endif %} +
    +
    + {{ code.friend_code }} +
    +
    + {% if not code.is_default %} +
    + {% csrf_token %} + +
    + {% endif %} + Delete +
    +
  • + {% endfor %} +
+ {% else %} +

You do not have any friend codes added yet.

+ {% endif %} + +
@@ -129,13 +180,11 @@
{% csrf_token %} {{ settings_form|crispy }} - {{ _('Edit Friend Codes') }} - +
+ +
- diff --git a/theme/templates/base.html b/theme/templates/base.html index cc4d325..ce033f6 100644 --- a/theme/templates/base.html +++ b/theme/templates/base.html @@ -31,22 +31,22 @@ - - + + {% tailwind_css %} - - + + - + - + @@ -58,18 +58,6 @@