Add daily-stone page showing a different mineral each day
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

New dailystone app with 207 minerals scraped from Wikipedia.
Each day displays a different mineral with photos, formula,
properties, description, and history. Page theme color matches
the mineral's typical appearance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 18:30:10 +03:00
parent a8ab5f6ce1
commit 0be99e8e9a
20 changed files with 6445 additions and 0 deletions

View File

@@ -0,0 +1,412 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if mineral %}{{ mineral.name }} — Daily Stone{% else %}Daily Stone{% endif %}</title>
<style>
:root {
--stone-color: {{ mineral.color_hex|default:"#6b7280" }};
--stone-light: color-mix(in srgb, var(--stone-color) 15%, #ffffff);
--stone-dark: color-mix(in srgb, var(--stone-color) 80%, #1a1a2e);
--stone-muted: color-mix(in srgb, var(--stone-color) 30%, #f5f5f0);
--stone-text: color-mix(in srgb, var(--stone-color) 60%, #1a1a2e);
--bg: #faf9f6;
--text: #2c2c2c;
--text-secondary: #5a5a5a;
--border: color-mix(in srgb, var(--stone-color) 25%, #d0d0d0);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Georgia', 'Times New Roman', serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
min-height: 100vh;
}
/* Top accent bar */
.accent-bar {
height: 6px;
background: linear-gradient(90deg, var(--stone-dark), var(--stone-color), var(--stone-dark));
}
.container {
max-width: 640px;
margin: 0 auto;
padding: 0 1.25rem 3rem;
}
/* Header */
.page-header {
text-align: center;
padding: 2rem 0 1rem;
border-bottom: 1px solid var(--border);
margin-bottom: 2rem;
}
.page-header .label {
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.page-header .date {
font-size: 0.85rem;
color: var(--text-secondary);
}
.mineral-name {
font-size: 2.4rem;
font-weight: normal;
color: var(--stone-text);
margin: 0.5rem 0 0.25rem;
line-height: 1.2;
}
.formula {
font-size: 1.15rem;
font-family: 'Courier New', monospace;
color: var(--text-secondary);
margin-bottom: 0.5rem;
letter-spacing: 0.02em;
}
/* Photo gallery */
.gallery {
margin: 1.5rem 0;
}
.gallery-main {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--stone-muted);
display: block;
}
.gallery-thumbs {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
overflow-x: auto;
}
.gallery-thumbs img {
width: 100px;
height: 75px;
object-fit: cover;
border-radius: 3px;
border: 2px solid transparent;
cursor: pointer;
flex-shrink: 0;
transition: border-color 0.2s;
}
.gallery-thumbs img:hover,
.gallery-thumbs img.active {
border-color: var(--stone-color);
}
/* Properties table */
.properties {
margin: 2rem 0;
border-top: 2px solid var(--stone-color);
}
.properties h2 {
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--stone-text);
padding: 0.75rem 0 0.5rem;
}
.prop-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.prop-item {
padding: 0.6rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.prop-item:nth-child(odd) {
padding-right: 1rem;
border-right: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.prop-item:nth-child(even) {
padding-left: 1rem;
}
.prop-label {
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-secondary);
display: block;
}
.prop-value {
font-size: 0.95rem;
color: var(--text);
margin-top: 0.1rem;
}
/* Color swatch */
.color-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
background: var(--stone-muted);
border-radius: 4px;
border: 1px solid var(--border);
}
.color-swatch {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--stone-color);
border: 2px solid color-mix(in srgb, var(--stone-color) 50%, #000);
flex-shrink: 0;
}
.color-info {
font-size: 0.9rem;
}
.color-info .hex {
font-family: 'Courier New', monospace;
color: var(--text-secondary);
font-size: 0.8rem;
}
/* Sections */
.section {
margin: 2rem 0;
}
.section h2 {
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--stone-text);
border-top: 2px solid var(--stone-color);
padding-top: 0.75rem;
margin-bottom: 0.75rem;
}
.section p {
margin-bottom: 1rem;
font-size: 0.95rem;
text-align: justify;
}
/* Footer */
.page-footer {
text-align: center;
padding-top: 2rem;
border-top: 1px solid var(--border);
margin-top: 3rem;
}
.page-footer a {
color: var(--stone-text);
text-decoration: none;
font-size: 0.85rem;
}
.page-footer a:hover {
text-decoration: underline;
}
.page-footer .home-link {
display: inline-block;
margin-top: 1rem;
font-size: 0.8rem;
color: var(--text-secondary);
}
/* Empty state */
.empty-state {
text-align: center;
padding: 4rem 1rem;
color: var(--text-secondary);
}
.empty-state h1 {
font-size: 2rem;
margin-bottom: 1rem;
font-weight: normal;
}
/* No images placeholder */
.no-image {
width: 100%;
aspect-ratio: 4/3;
background: var(--stone-muted);
border: 1px dashed var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="accent-bar"></div>
<div class="container">
{% if mineral %}
<header class="page-header">
<div class="label">Daily Stone</div>
<h1 class="mineral-name">{{ mineral.name }}</h1>
{% if mineral.formula %}<div class="formula">{{ mineral.formula }}</div>{% endif %}
<div class="date">{{ today|date:"F j, Y" }}</div>
</header>
<!-- Gallery -->
<div class="gallery">
{% if mineral.image_urls %}
<img class="gallery-main" id="mainImage"
src="{{ mineral.image_urls.0 }}"
alt="{{ mineral.name }}">
{% if mineral.image_urls|length > 1 %}
<div class="gallery-thumbs">
{% for url in mineral.image_urls %}
<img src="{{ url }}"
alt="{{ mineral.name }}"
class="{% if forloop.first %}active{% endif %}"
onclick="switchImage(this, '{{ url }}')">
{% endfor %}
</div>
{% endif %}
{% else %}
<div class="no-image">No image available</div>
{% endif %}
</div>
<!-- Color -->
<div class="color-row">
<div class="color-swatch"></div>
<div class="color-info">
{% if mineral.color_description %}
{{ mineral.color_description }}
{% else %}
Typical color
{% endif %}
<br><span class="hex">{{ mineral.color_hex }}</span>
</div>
</div>
<!-- Properties -->
<div class="properties">
<h2>Properties</h2>
<div class="prop-grid">
{% if mineral.category %}
<div class="prop-item">
<span class="prop-label">Category</span>
<span class="prop-value">{{ mineral.category }}</span>
</div>
{% endif %}
{% if mineral.crystal_system %}
<div class="prop-item">
<span class="prop-label">Crystal System</span>
<span class="prop-value">{{ mineral.crystal_system }}</span>
</div>
{% endif %}
{% if mineral.mohs_hardness %}
<div class="prop-item">
<span class="prop-label">Hardness (Mohs)</span>
<span class="prop-value">{{ mineral.mohs_hardness }}</span>
</div>
{% endif %}
{% if mineral.luster %}
<div class="prop-item">
<span class="prop-label">Luster</span>
<span class="prop-value">{{ mineral.luster }}</span>
</div>
{% endif %}
{% if mineral.streak %}
<div class="prop-item">
<span class="prop-label">Streak</span>
<span class="prop-value">{{ mineral.streak }}</span>
</div>
{% endif %}
{% if mineral.specific_gravity %}
<div class="prop-item">
<span class="prop-label">Specific Gravity</span>
<span class="prop-value">{{ mineral.specific_gravity }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Description -->
{% if mineral.description %}
<div class="section">
<h2>About</h2>
{% for para in mineral.description.splitlines %}
{% if para %}<p>{{ para }}</p>{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- History -->
{% if mineral.history %}
<div class="section">
<h2>History &amp; Etymology</h2>
{% for para in mineral.history.splitlines %}
{% if para %}<p>{{ para }}</p>{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- Footer -->
<footer class="page-footer">
{% if mineral.wikipedia_url %}
<a href="{{ mineral.wikipedia_url }}" target="_blank" rel="noopener">
Read more on Wikipedia &rarr;
</a>
{% endif %}
<br>
<a href="/" class="home-link">k-boris.tech</a>
</footer>
<script>
function switchImage(thumb, url) {
document.getElementById('mainImage').src = url;
document.querySelectorAll('.gallery-thumbs img').forEach(
img => img.classList.remove('active')
);
thumb.classList.add('active');
}
</script>
{% else %}
<div class="empty-state">
<h1>Daily Stone</h1>
<p>No minerals have been loaded yet. Check back soon!</p>
<a href="/" class="home-link">k-boris.tech</a>
</div>
{% endif %}
</div>
</body>
</html>