Compare commits

..

3 Commits

Author SHA1 Message Date
Disco DeDisco
fe6d2c5db1 stylistic changes primarily, esp. to page titles(new spans in header_text block, for instance)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-10 01:25:07 -04:00
Disco DeDisco
d2861077a4 tooltips now fully styled, appearing above applet container to avoid clipping issues; new methods added to apps.lyric.models.Token 2026-03-09 23:48:20 -04:00
Disco DeDisco
645b265c80 several user QoL styling improvements, incl. footer icon .active color painting 2026-03-09 22:42:30 -04:00
16 changed files with 250 additions and 70 deletions

View File

@@ -0,0 +1,24 @@
function initGameKitTooltips() {
const portal = document.getElementById('id_tooltip_portal');
if (!portal) return;
document.querySelectorAll('#id_game_kit .token').forEach(token => {
const tooltip = token.querySelector('.token-tooltip');
if (!tooltip) return;
token.addEventListener('mouseenter', () => {
const rect = token.getBoundingClientRect();
portal.innerHTML = tooltip.innerHTML;
portal.style.left = Math.round(rect.left + rect.width / 2) + 'px';
portal.style.top = Math.round(rect.top) + 'px';
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
portal.classList.add('active');
});
token.addEventListener('mouseleave', () => {
portal.classList.remove('active');
});
});
}
document.addEventListener('DOMContentLoaded', initGameKitTooltips);

View File

@@ -65,20 +65,33 @@ class Token(models.Model):
token_type = models.CharField(max_length=8, choices=TOKEN_TYPE_CHOICES) token_type = models.CharField(max_length=8, choices=TOKEN_TYPE_CHOICES)
expires_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True)
def tooltip_text(self): def tooltip_name(self):
if self.token_type == self.COIN:
return (
"Coin-on-a-String: Admit 1 Entry"
" (and another after that, and another after that\u2026)"
" \u2014 no expiry"
)
if self.token_type == self.FREE:
return (
f"Free Token: Admit 1 Entry"
f" \u2014 Expires {self.expires_at.strftime('%Y-%m-%d')}"
)
return self.get_token_type_display() return self.get_token_type_display()
def tooltip_description(self):
if self.token_type in (self.COIN, self.FREE):
return "Admit 1 Entry"
return ""
def tooltip_expiry(self):
if self.token_type == self.COIN:
return "no expiry"
if self.expires_at:
return f"Expires {self.expires_at.strftime('%Y-%m-%d')}"
return ""
def tooltip_shoptalk(self):
if self.token_type == self.COIN:
return "\u2026and another after that, and another after that\u2026"
return None
def tooltip_text(self):
text = f"{self.tooltip_name()}: {self.tooltip_description()}"
if self.tooltip_shoptalk():
text += f" ({self.tooltip_shoptalk()})"
text += f" \u2014 {self.tooltip_expiry()}"
return text
class PaymentMethod(models.Model): class PaymentMethod(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods") user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods")
stripe_pm_id = models.CharField(max_length=255) stripe_pm_id = models.CharField(max_length=255)

View File

@@ -37,9 +37,12 @@ class FunctionalTest(StaticLiveServerTestCase):
# Helper methods # Helper methods
def setUp(self): def setUp(self):
options = webdriver.FirefoxOptions() options = webdriver.FirefoxOptions()
if os.environ.get("HEADLESS"): headless = os.environ.get("HEADLESS")
if headless:
options.add_argument("--headless") options.add_argument("--headless")
self.browser = webdriver.Firefox(options=options) self.browser = webdriver.Firefox(options=options)
if headless:
self.browser.set_window_size(1366, 900)
self.test_server = os.environ.get("TEST_SERVER") self.test_server = os.environ.get("TEST_SERVER")
if self.test_server: if self.test_server:
self.live_server_url = 'http://' + self.test_server self.live_server_url = 'http://' + self.test_server

View File

@@ -16,7 +16,7 @@ class MyListsPage:
self.test.wait_for( self.test.wait_for(
lambda: self.test.assertIn( lambda: self.test.assertIn(
email, email,
self.test.browser.find_element(By.TAG_NAME, "h2").text, self.test.browser.find_element(By.TAG_NAME, "h2").text.lower(),
) )
) )
return self return self

View File

@@ -56,18 +56,15 @@ class GameboardNavigationTest(FunctionalTest):
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn") self.browser.find_element(By.CSS_SELECTOR, ".gear-btn")
# 3. Assert Coin-on-a-String present in kit # 3. Assert Coin-on-a-String present in kit
coin = self.browser.find_element(By.ID, "id_kit_coin_on_a_string") coin = self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
self.browser.execute_script("arguments[0].scrollIntoView({block: 'center'});", coin)
# 6. Hover over it; assert tooltip shows name, entry text & reuse description # 6. Hover over it; assert tooltip shows name, entry text & reuse description
ActionChains(self.browser).move_to_element(coin).perform() ActionChains(self.browser).move_to_element(coin).perform()
self.wait_for( self.wait_for(
lambda: self.assertTrue( lambda: self.assertTrue(
self.browser.find_element( self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
By.CSS_SELECTOR, "#id_kit_coin_on_a_string .token-tooltip"
).is_displayed()
) )
) )
coin_tooltip = self.browser.find_element( coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
By.CSS_SELECTOR, "#id_kit_coin_on_a_string .token-tooltip"
).text
self.assertIn("Coin-on-a-String", coin_tooltip) self.assertIn("Coin-on-a-String", coin_tooltip)
self.assertIn("Admit 1 Entry", coin_tooltip) self.assertIn("Admit 1 Entry", coin_tooltip)
self.assertIn("and another after that", coin_tooltip) self.assertIn("and another after that", coin_tooltip)
@@ -77,14 +74,10 @@ class GameboardNavigationTest(FunctionalTest):
ActionChains(self.browser).move_to_element(free_token).perform() ActionChains(self.browser).move_to_element(free_token).perform()
self.wait_for( self.wait_for(
lambda: self.assertTrue( lambda: self.assertTrue(
self.browser.find_element( self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
By.CSS_SELECTOR, "#id_kit_free_token_0 .token-tooltip"
).is_displayed()
) )
) )
free_tooltip = self.browser.find_element( free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
By.CSS_SELECTOR, "#id_kit_free_token_0 .token-tooltip"
).text
self.assertIn("Free Token", free_tooltip) self.assertIn("Free Token", free_tooltip)
self.assertIn("Admit 1 Entry", free_tooltip) self.assertIn("Admit 1 Entry", free_tooltip)
self.assertIn("Expires", free_tooltip) self.assertIn("Expires", free_tooltip)

View File

@@ -24,9 +24,9 @@
right: 0.5rem; right: 0.5rem;
z-index: 100; z-index: 100;
background-color: rgba(var(--priUser), 0.95); background-color: rgba(var(--priUser), 0.95);
border: 0.15rem solid rgba(var(--secUser), 0.5); border: 0.15rem solid rgba(var(--secUser), 1);
box-shadow: box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5), 0 0 0.5rem rgba(var(--secUser), 0.75),
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
; ;
border-radius: 0.75rem; border-radius: 0.75rem;
@@ -97,7 +97,13 @@
); );
section { section {
border: 0.2rem solid rgba(var(--secUser), 0.5); border:
0.2rem solid rgba(var(--secUser), 0.5),
;
box-shadow:
inset -1px -1px 0 rgba(255, 255, 255, 0.125),
inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8)
;
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem; padding: 1rem;
overflow: hidden; overflow: hidden;

View File

@@ -19,6 +19,7 @@ body {
.container { .container {
max-width: 960px; max-width: 960px;
width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
flex: 1; flex: 1;
@@ -139,19 +140,57 @@ body {
} }
.col-lg-6 { .col-lg-6 {
max-width: 600px; max-width: inherit;
margin: 0 auto; margin: 0 1rem;
h2 { h2 {
font-size: 2.5rem; font-size: 3rem;
color: rgba(var(--quaUser), 1); color: rgba(var(--secUser), 0.6);
margin-bottom: 1rem; margin-bottom: 1rem;
text-align: justify;
text-align-last: justify;
text-justify: inter-character;
text-transform: uppercase;
text-shadow:
1px 1px 0 rgba(255, 255, 255, 0.125), // highlight (up-left)
-0.125rem -0.125rem 0 rgba(0, 0, 0, 0.8) // shadow (down-right)
;
span {
color: rgba(var(--quaUser), 0.6);
}
} }
} }
} }
} }
} }
@media (min-width: 1200px) {
body .container {
max-width: 1200px;
}
}
@media (min-width: 1024px) and (max-height: 700px) {
body .container .navbar {
padding: 0.5rem 0;
.navbar-brand h1 {
font-size: 1.4rem;
}
}
#id_footer {
height: 3.5rem;
padding: 0.7rem 1rem;
gap: 0.35rem;
#id_footer_nav a {
font-size: 1.2rem;
}
}
}
#id_footer { #id_footer {
flex-shrink: 0; flex-shrink: 0;
height: 5rem; height: 5rem;
@@ -175,11 +214,18 @@ body {
a { a {
font-size: 1.75rem; font-size: 1.75rem;
color: rgba(var(--quaUser), 0.9); color: rgba(var(--secUser), 0.6);
text-shadow: text-shadow:
0 0 0.25rem rgba(0, 0, 0, 0.5), 0 0 0.25rem rgba(0, 0, 0, 0.25),
; ;
&.active {
color: rgba(var(--quaUser), 1);
text-shadow:
0 0 0.5rem rgba(0, 0, 0, 0.5),
;
}
&:hover { &:hover {
color: rgba(var(--quaUser), 1); color: rgba(var(--quaUser), 1);
text-shadow: text-shadow:

View File

@@ -11,6 +11,7 @@ body.page-dashboard {
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.row { .row {

View File

@@ -11,6 +11,7 @@ body.page-gameboard {
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.row { .row {
@@ -41,24 +42,60 @@ body.page-gameboard {
} }
} }
#id_game_applets_container { #id_applet_game_kit {
#id_applet_game_kit { display: flex;
display: flex; flex-direction: column;
flex-direction: column;
#id_game_kit { #id_game_kit {
flex: 1; flex: 1;
display: flex; position: relative;
flex-direction: row; display: flex;
align-items: center; flex-direction: row;
gap: 0.75rem; align-items: center;
overflow-x: auto; justify-content: space-evenly;
scrollbar-width: none; overflow-x: visible;
&::-webkit-scrollbar { display: none; } scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
.token { position: static; }
.token:hover .token-tooltip { display: none; }
.token,
.kit-item { font-size: 1.5rem; }
.kit-item { opacity: 0.6; }
}
}
#id_applet_new_game,
#id_applet_my_games {
display: flex;
flex-direction: column;
ul {
flex: 1;
list-style: none;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
small {
opacity: 0.5;
font-style: italic;
} }
} }
} }
#id_tooltip_portal {
position: fixed;
z-index: 9999;
&.active { display: block; }
}
@media (max-height: 500px) { @media (max-height: 500px) {
body.page-gameboard { body.page-gameboard {
.container { .container {

View File

@@ -1,27 +1,52 @@
.token-tooltip {
display: none;
width: 16rem;
max-width: 16rem;
white-space: normal;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
border: 0.1rem solid rgba(var(--secUser), 0.5);
color: rgba(var(--secUser), 1);
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
z-index: 10;
font-size: 0.875rem;
h4 {
font-size: 0.95rem;
margin: 0 0 0.3rem 0;
color: rgba(var(--terUser), 1);
}
p {
margin: 0 0 0.2rem 0;
&.expiry {
color: rgba(var(--priRd), 1);
}
}
small {
display: block;
font-size: 0.6rem;
opacity: 0.6;
}
}
.token { .token {
position: relative; position: relative;
display: inline-block; display: inline-block;
cursor: pointer; cursor: help;
color: rgba(var(--terUser), 1); color: rgba(var(--terUser), 1);
.token-tooltip { .token-tooltip {
display: none;
position: absolute; position: absolute;
bottom: 125%; bottom: 125%;
left: 0; left: 50%;
width: 16rem; transform: translateX(-50%);
max-width: 16rem;
white-space: normal;
background-color: rgba(var(--priUser), 0.95);
border: 0.1rem solid rgba(var(--secUser), 0.5);
color: rgba(var(--secUser), 1);
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
z-index: 10;
font-size: 0.875rem;
} }
&:hover .token-tooltip { &:hover .token-tooltip {
display: block; display: block;
} }
} }

View File

@@ -2,7 +2,7 @@
{% load lyric_extras %} {% load lyric_extras %}
{% block title_text %}Dashboard{% endblock title_text %} {% block title_text %}Dashboard{% endblock title_text %}
{% block header_text %}Dashboard{% endblock header_text %} {% block header_text %}<span>Dash</span>board{% endblock header_text %}
{% block scripts %} {% block scripts %}
{% include "apps/dashboard/_partials/_scripts.html" %} {% include "apps/dashboard/_partials/_scripts.html" %}

View File

@@ -1,6 +1,9 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load static %} {% load static %}
{% block title_text %}Dashboard{% endblock title_text %}
{% block header_text %}<span>Dash</span>board{% endblock header_text %}
{% block content %} {% block content %}
<div class="wallet-page"> <div class="wallet-page">
<h1>Wallet</h1> <h1>Wallet</h1>

View File

@@ -7,13 +7,31 @@
{% if coin %} {% if coin %}
<div id="id_kit_coin_on_a_string" class="token"> <div id="id_kit_coin_on_a_string" class="token">
<i class="fa-solid fa-clover"></i> <i class="fa-solid fa-clover"></i>
<span class="token-tooltip">{{ coin.tooltip_text }}</span> <div class="token-tooltip">
<h4>{{ coin.tooltip_name }}</h4>
<p>
{{ coin.tooltip_description }}
</p>
{% if coin.tooltip_shoptalk %}
<small><em>{{ coin.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ coin.tooltip_expiry }}</p>
</div>
</div> </div>
{% endif %} {% endif %}
{% for token in free_tokens %} {% for token in free_tokens %}
<div id="id_kit_free_token_{{ forloop.counter0 }}" class="token"> <div id="id_kit_free_token_{{ forloop.counter0 }}" class="token">
<i class="fa-solid fa-coins"></i> <i class="fa-solid fa-coins"></i>
<span class="token-tooltip">{{ token.tooltip_text }}</span> <div class="token-tooltip">
<h4>{{ token.tooltip_name }}</h4>
<p>
{{ token.tooltip_description }}
</p>
{% if token.tooltip_shoptalk %}
<small><em>{{ token.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ token.tooltip_expiry }}</p>
</div>
</div> </div>
{% endfor %} {% endfor %}
<div id="id_kit_card_deck" class="kit-item"><i class="fa-regular fa-id-badge"></i></div> <div id="id_kit_card_deck" class="kit-item"><i class="fa-regular fa-id-badge"></i></div>

View File

@@ -1,11 +1,17 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load static %}
{% block title_text %}Gameboard{% endblock title_text %} {% block title_text %}Gameboard{% endblock title_text %}
{% block header_text %}Gameboard{% endblock header_text %} {% block header_text %}<span>Game</span>board{% endblock header_text %}
{% block content %} {% block content %}
<div class="gameboard-page"> <div class="gameboard-page">
{% include "apps/applets/_partials/_gear.html" with menu_id="id_game_applet_menu" %} {% include "apps/applets/_partials/_gear.html" with menu_id="id_game_applet_menu" %}
{% include "apps/gameboard/_partials/_applets.html" %} {% include "apps/gameboard/_partials/_applets.html" %}
</div> </div>
{% endblock content %} <div id="id_tooltip_portal" class="token-tooltip"></div>
{% endblock content %}
{% block scripts %}
<script src="{% static 'apps/gameboard/gameboard.js' %}"></script>
{% endblock scripts %}

View File

@@ -1,7 +1,11 @@
<footer id="id_footer"> <footer id="id_footer">
<nav id="id_footer_nav"> <nav id="id_footer_nav">
<a href="/"><i class="fa-solid fa-gauge"></i></a> <a href="/" class="{% if request.path == '/' or '/dashboard/' in request.path %}active{% endif %}">
<a href="/gameboard/"><i class="fa-solid fa-chess-board"></i></a> <i class="fa-solid fa-gauge"></i>
</a>
<a href="/gameboard/" class="{% if '/gameboard/' in request.path %}active{% endif %}">
<i class="fa-solid fa-chess-board"></i>
</a>
</nav> </nav>
<div class="footer-container"> <div class="footer-container">
<small>&copy;{% now "Y" %} Dis Co.</small> <small>&copy;{% now "Y" %} Dis Co.</small>

View File

@@ -32,6 +32,7 @@
placeholder="your@email.here" placeholder="your@email.here"
> >
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-confirm">OK</button>
</div> </div>
</form> </form>
{% endif %} {% endif %}