Merge branch 'dark-fucking-mode' into 'master'

Implement comprehensive dark mode functionality

See merge request engineering/make-post-sell/make_post_sell!54
This commit is contained in:
Russell Ballestrini 2025-10-04 21:09:51 +00:00
commit 8a014a6e61
21 changed files with 965 additions and 109 deletions

View file

@ -93,31 +93,29 @@ When making changes to database models, always create Alembic migrations:
# Activate environment first
source env/bin/activate
# Create a new migration
# Create a new migration (manual)
alembic -c data/development.ini revision -m "description of change"
# Edit the generated migration file in make_post_sell/scripts/alembic/versions/
# Add your upgrade() and downgrade() logic
# OR: Create autogenerated migration (compares DB with models)
alembic -c data/development.ini revision --autogenerate -m "description of change"
# Apply the migration
alembic -c data/development.ini upgrade head
# Edit the generated migration file in make_post_sell/scripts/alembic/versions/
```
### Migration Commands
### Running Migrations
```bash
# Check current database revision
# Apply all pending migrations
alembic -c data/development.ini upgrade head
# Check current migration status
alembic -c data/development.ini current
# View migration history
alembic -c data/development.ini history
# Upgrade to latest
alembic -c data/development.ini upgrade head
# Auto-generate migration from model changes (review before applying!)
alembic -c data/development.ini revision --autogenerate -m "auto-generated changes"
```
**IMPORTANT**: Always backup the database before running migrations!
### Important Migration Notes
**SQLite Column Defaults**: When adding NOT NULL columns with defaults to existing tables in SQLite, use `server_default` with raw SQL values:

329
THEME_AUDIT.md Normal file
View file

@ -0,0 +1,329 @@
# CSS Theme Audit Report
## Executive Summary
This audit documents all DOM elements, CSS classes, and inline styles across the Make Post Sell application templates to prepare for theme implementation. The application uses a mobile-first CSS Grid layout with semantic class naming.
## CSS Architecture Overview
- **Layout System**: CSS Grid (exclusively, no Flexbox)
- **Mobile-First**: Responsive design starting from mobile
- **Main CSS File**: `make_post_sell/static/css/common.css` (1,072 lines)
- **Template Engine**: Jinja2 (.j2 files)
- **Total Templates**: 42 templates across 48 files
## 1. INLINE STYLES AUDIT
### High Priority - Need CSS Classes
**shop_settings.j2** (16 instances)
- `style="color: #666;"` - Status messages (lines 176, 271, 314, 329, 392)
- `style="color: green;"` - Success indicators (lines 308, 366)
- `style="color: #d44;"` - Error indicators (lines 312, 370)
- `style="display: inline;"` - Inline labels (lines 136, 266)
- `style="width: 32px;"` - Favicon preview (line 434)
**crypto_checkout.j2** (25 instances)
- Complex grid layouts: `style="display: grid; grid-template-columns: auto 1fr; gap: 20px;"` (line 23)
- Button layouts: `style="display: grid; grid-template-columns: 1fr 1fr 1fr;"` (line 38)
- Status boxes: `style="background: #f8f9fa; border: 1px solid #dee2e6;"` (line 48)
- Warning/info boxes: `style="background: #d1fae5; border: 1px solid #10b981;"` (line 539)
**cart_checkout.j2** (18 instances)
- Payment method buttons with crypto icons
- Status indicators and warning boxes
- Grid layouts for pending payment display
**cart.j2** (16 instances)
- Float layouts: `style="float: right;"` (line 34)
- Form styling: `style="display: inline;"` (lines 113, 121)
- Shop names: `style="font-size: 1.5em;"` (line 90)
### Medium Priority
**snippets/comments.j2** (6 instances)
- Comment indentation: `style="margin-left: {{ comment.depth * 20 }}px;"`
- Form hiding: `style="display: none;"`
**product.j2**, **content.j2** (2 instances each)
- Subtitle styling: `style="font-size: 0.6em; font-weight: normal;"`
**Other templates** (30+ instances across remaining files)
- Various display, color, and layout inline styles
## 2. CSS CLASSES ANALYSIS
### Well-Designed Semantic Classes (Keep)
**Layout Classes**
- `.one-column` (24 uses) - Single column layout, max-width 600px
- `.two-column` (4 uses) - Two column responsive layout
- `.one-column-thin` (1 use) - Narrower single column, max-width 400px
- `.well`, `.well2` (9 uses) - Content containers with background
**Component Classes**
- `.mps-button` family - Comprehensive button system
- `.serp` family - Search results display
- `.cart-*` family - Cart-specific components
- `.product-*` family - Product display components
- `.comment-*` family - Comment system components
**State Classes**
- `.hidden-control` - Toggle visibility
- `.opacity-60` - Transparency
- `.discounted` - Price styling
- `.highlight` - Text highlighting
### Button System Analysis
**Primary Buttons** (semantic and consistent)
```css
.mps-button - Base button class
.mps-button-blue - #98b6fa (info/secondary)
.mps-button-green - #a3c765 (success/primary)
.mps-button-red - #bc2131 (danger)
.mps-button-small - Smaller variant for desktop
```
**Specialized Buttons** (good semantic naming)
```css
.cart-checkout-button
.cart-save-button
.cart-activate-button
.cart-delete-button
.product-download-button
.product-edit-button
.shop-new-button
.payment-toggle-button
```
### Areas Needing Attention
**Inconsistent Classes**
- Multiple button size combinations (`.mps-button-small mps-button-green`)
- Mixed semantic levels (`.product-edit-button mps-button mps-button-small`)
**Missing Classes** (have inline styles instead)
- Status indicators (success, warning, error)
- Crypto payment layouts
- Form spacing
- Icon sizing
## 3. COLOR PALETTE ANALYSIS
### Current Theme Colors
```css
/* Primary Brand Colors */
#5871ad - Primary blue (navbar, buttons)
#82A8FF - Gradient end
#98b6fa - Secondary blue (info buttons)
/* Action Colors */
#a3c765 - Success green (primary actions)
#bc2131 - Danger red (destructive actions)
#CC6958 - Delete red (cart delete)
/* Status Colors */
#28a745 - Success green (confirmations)
#ffc107 - Warning yellow
#dc3545 - Error red
#6c757d - Muted gray
/* Background Colors */
#F9F9FA - Light gray (navbar, wells)
#f2f2f2 - Even table rows
#ffffff - Odd table rows, content background
/* Text Colors */
#5f6368 - Link color
#666666 - Muted text
#721c24 - Error text
#856404 - Warning text
#0c5460 - Info text
#155724 - Success text
```
## 4. LAYOUT SYSTEM ANALYSIS
### CSS Grid Implementation
- Mobile-first responsive design
- Breakpoints: 800px (tablet), 960px (desktop), 1200px (large)
- Grid areas for complex layouts (product pages)
- Auto-fit columns for responsive grids
### Layout Classes Usage
```
one-column: 24 instances - Most common layout
shop-settings well: 9 instances - Settings pages
two-column: 4 instances - Product/content pages
well: 7 instances - Content containers
```
## 5. THEME TARGETS FOR IMPLEMENTATION
### Essential CSS Variables Needed
```css
:root {
/* Brand Colors */
--color-primary: #5871ad;
--color-primary-light: #82A8FF;
--color-secondary: #98b6fa;
/* Action Colors */
--color-success: #a3c765;
--color-danger: #bc2131;
--color-warning: #ffc107;
--color-info: #0c5460;
/* Status Colors */
--color-status-success: #28a745;
--color-status-error: #dc3545;
--color-status-warning: #856404;
--color-status-muted: #6c757d;
/* Background Colors */
--bg-navbar: #F9F9FA;
--bg-content: #ffffff;
--bg-well: #F9F9FA;
--bg-table-even: #f2f2f2;
/* Text Colors */
--text-link: #5f6368;
--text-muted: #666666;
--text-body: #333333;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 14px;
--spacing-lg: 20px;
--spacing-xl: 40px;
}
```
### New Classes Needed (Replace Inline Styles)
```css
/* Status Indicators */
.status-success { color: var(--color-status-success); }
.status-error { color: var(--color-status-error); }
.status-warning { color: var(--color-status-warning); }
.status-muted { color: var(--color-status-muted); }
/* Crypto Payment Layouts */
.crypto-payment-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
margin: 16px 0;
align-items: start;
}
.crypto-button-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
margin: 8px 0;
}
/* Info Boxes */
.info-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
margin: 10px 0;
}
.success-box {
background: #d1fae5;
border: 1px solid #10b981;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.warning-box {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
/* Form Elements */
.inline-label { display: inline; }
.form-row { margin-bottom: var(--spacing-md); }
/* Icon Sizes */
.icon-sm { width: 32px; height: 32px; }
.icon-md { width: 48px; height: 48px; }
.icon-lg { width: 150px; height: 150px; }
/* Typography */
.text-large { font-size: 1.5em; }
.text-small { font-size: 0.6em; font-weight: normal; }
.text-monospace { font-family: monospace; }
/* Layout Utilities */
.float-right { float: right; }
.text-right { text-align: right; }
.margin-top-none { margin-top: 0; }
.margin-bottom-none { margin-bottom: 0; }
```
## 6. RECOMMENDATIONS
### High Priority
1. **Replace all inline styles** with semantic CSS classes
2. **Implement CSS custom properties** for theme colors
3. **Create component-specific classes** for crypto payments, status indicators
4. **Establish consistent spacing system** using CSS variables
### Medium Priority
1. **Consolidate button variants** to reduce complexity
2. **Create utility classes** for common patterns (float, text-align, etc.)
3. **Implement theme switching mechanism** using CSS custom properties
4. **Add dark mode support** with color scheme media queries
### Low Priority
1. **Optimize CSS delivery** by splitting into component files
2. **Add CSS Grid fallbacks** for older browsers if needed
3. **Implement print styles** for invoices and receipts
## 7. THEME IMPLEMENTATION STRATEGY
### Phase 1: Foundation
- Convert all inline styles to CSS classes
- Implement CSS custom properties system
- Create component-specific classes
### Phase 2: Theming
- Build theme switching system
- Create default light/dark themes
- Add theme preview functionality
### Phase 3: Customization
- Allow color customization per shop
- Implement theme marketplace/sharing
- Add advanced theming options
## Files Modified in This Audit
- `make_post_sell/static/css/common.css` (analyzed)
- All 48 template files in `make_post_sell/templates/` (analyzed)
## Theme Target Summary
The application is well-structured for theming with:
- ✅ Semantic CSS class naming
- ✅ CSS Grid layout system
- ✅ Mobile-first responsive design
- ✅ Consistent button system
- ❌ Too many inline styles (83 instances)
- ❌ Missing CSS custom properties
- ❌ No theme switching mechanism
**Total Elements Requiring Attention**: 83 inline styles across 21 template files
**Estimated Effort**: Medium (2-3 weeks for complete theme system)
**Theme Readiness**: 65% (needs cleanup before theming)

View file

@ -103,6 +103,9 @@ class Shop(RBase, Base):
BigInteger, nullable=False, default=3600
) # Default 60 minutes
# Default theme for shop visitors (0=dark, 1=light)
default_theme = Column(BigInteger, nullable=False, default=1) # Default light mode
# many to many uses association_proxy.
users = association_proxy("shop_users", "user", creator=lambda u: UserShop(user=u))

View file

@ -0,0 +1,32 @@
"""add default_theme to shop
Revision ID: b090f873502e
Revises: 16b9fd6d0f70
Create Date: 2025-10-04 14:11:13.516680
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "b090f873502e"
down_revision = "16b9fd6d0f70"
branch_labels = None
depends_on = None
from make_post_sell.models.meta import UUIDType
def upgrade():
# Add default_theme column to mps_shop table with default 1 (light mode)
op.add_column(
"mps_shop",
sa.Column("default_theme", sa.BigInteger(), nullable=False, server_default="1"),
)
def downgrade():
# Remove default_theme column from mps_shop table
op.drop_column("mps_shop", "default_theme")

View file

@ -272,6 +272,7 @@ img.product-main {
border: none;
border-radius: 4px;
color: white;
background-color: #D4D0C8;
display: inline-block;
font-size: 14px;
font-weight: bold;
@ -285,21 +286,92 @@ img.product-main {
cursor: pointer;
}
[data-theme="dark"] .mps-button {
background-color: #2d3748;
background-image: url("/static/img/trans-blue.png");
color: var(--blue-color);
border: 2px solid var(--blue-color);
}
[data-theme="dark"] .mps-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-blue2.png");
color: #ffffff;
border: 2px solid var(--blue-color);
}
a.mps-button-blue,
button.mps-button-blue {
background-color: #98b6fa;
}
[data-theme="dark"] a.mps-button-blue,
[data-theme="dark"] button.mps-button-blue,
[data-theme="dark"] input.mps-button-blue {
background-color: #2d3748;
background-image: url("/static/img/trans-blue.png");
color: var(--blue-color);
border: 2px solid var(--blue-color);
}
[data-theme="dark"] a.mps-button-blue:hover,
[data-theme="dark"] button.mps-button-blue:hover,
[data-theme="dark"] input.mps-button-blue:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-blue2.png");
color: #ffffff;
border: 2px solid var(--blue-color);
}
a.mps-button-green,
button.mps-button-green {
background-color: #a3c765;
}
[data-theme="dark"] a.mps-button-green,
[data-theme="dark"] button.mps-button-green,
[data-theme="dark"] input.mps-button-green {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
color: var(--green-color);
border: 2px solid var(--green-color);
}
[data-theme="dark"] a.mps-button-green:hover,
[data-theme="dark"] button.mps-button-green:hover,
[data-theme="dark"] input.mps-button-green:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
a.mps-button-red,
button.mps-button-red {
button.mps-button-red,
input.mps-button-red {
background-color: #bc2131;
opacity: 0.8;
}
[data-theme="dark"] a.mps-button-red,
[data-theme="dark"] button.mps-button-red,
[data-theme="dark"] input.mps-button-red {
background-color: #2d3748;
background-image: url("/static/img/trans-red.png");
color: var(--red-color);
border: 2px solid var(--red-color);
opacity: 1.0;
}
[data-theme="dark"] a.mps-button-red:hover,
[data-theme="dark"] button.mps-button-red:hover,
[data-theme="dark"] input.mps-button-red:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-red2.png");
color: #ffffff;
border: 2px solid var(--red-color);
}
.mps-cancel-button,
.mps-comment-form-button {
display: inline-block;
@ -374,6 +446,67 @@ a.product-edit-button {
background-color: #98b6fa;
}
[data-theme="dark"] a.mps-button.product-edit-button {
background-color: #2d3748;
background-image: url("/static/img/trans-blue.png");
color: #7ab9ff;
border: 2px solid #7ab9ff;
}
[data-theme="dark"] a.mps-button.product-edit-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-blue2.png");
color: #ffffff;
border: 2px solid #7ab9ff;
}
[data-theme="dark"] a.mps-button.shop-new-button {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
color: var(--green-color);
border: 2px solid var(--green-color);
}
[data-theme="dark"] a.mps-button.shop-new-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
/* Product download and preview buttons - make them green in dark mode */
[data-theme="dark"] a.product-download-button,
[data-theme="dark"] button.product-download-button {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
color: var(--green-color);
border: 2px solid var(--green-color);
}
[data-theme="dark"] a.product-download-button:hover,
[data-theme="dark"] button.product-download-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
[data-theme="dark"] a.product-preview-button,
[data-theme="dark"] button.product-preview-button {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
color: var(--green-color);
border: 2px solid var(--green-color);
}
[data-theme="dark"] a.product-preview-button:hover,
[data-theme="dark"] button.product-preview-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
a.product-preview-button {
background-color: #98b6fa;
}
@ -436,6 +569,20 @@ button.cart-remove-link:hover {
text-decoration: underline;
}
/* Links should use the theme link color */
[data-theme="dark"] a {
color: var(--link-color);
}
[data-theme="dark"] button.cart-remove-link {
color: var(--blue-color);
}
[data-theme="dark"] button.cart-remove-link:hover {
color: var(--blue-color);
text-decoration: underline;
}
/* “Add To Cart”, “Download”, “Preview” buttons */
button.product-download-button,
button.product-preview-button {
@ -455,6 +602,21 @@ button.cart-checkout-button {
button.cart-save-button {
background-color: #98b6fa;
}
/* Dark mode cart buttons */
[data-theme="dark"] button.cart-checkout-button {
background-color: #2d3748;
background-image: url("/static/img/trans-green.png");
color: var(--green-color);
border: 2px solid var(--green-color);
}
[data-theme="dark"] button.cart-checkout-button:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-green2.png");
color: #ffffff;
border: 2px solid var(--green-color);
}
button.cart-activate-button {
background-color: #5871ad;
}
@ -519,6 +681,20 @@ div.serp-item:hover {
filter: brightness(88%);
}
[data-theme="dark"] div.serp-item:hover {
background-color: #2d3748;
background-image: url("/static/img/trans-blue2.png");
border: 2px solid #7ab9ff;
color: #ffffff;
-webkit-filter: none;
-moz-filter: none;
filter: none;
}
[data-theme="dark"] div.serp-item:hover a {
color: #ffffff;
}
img.serp-thumbnail {
border-radius: 4px;
}
@ -1041,9 +1217,9 @@ div.message-ribbon {
}
.alert-info, .alert-notice {
color: #0c5460;
background-color: #98b6fa;
border-color: #bee5eb;
color: var(--text-primary);
background-color: color-mix(in srgb, var(--info-color) 20%, var(--bg-primary) 80%);
border-color: var(--info-color);
}
.alert-success {
@ -1143,8 +1319,9 @@ div.message-ribbon {
}
.status-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 4px;
padding: 15px;
margin: 10px 0;
@ -1167,16 +1344,18 @@ div.message-ribbon {
}
.success-notice {
background: #d1fae5;
border: 1px solid #10b981;
background: color-mix(in srgb, var(--success-color) 15%, var(--bg-primary) 85%);
border: 1px solid var(--success-color);
color: var(--text-primary);
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.warning-notice {
background: #fef3c7;
border: 1px solid #f59e0b;
background: color-mix(in srgb, var(--warning-color) 15%, var(--bg-primary) 85%);
border: 1px solid var(--warning-color);
color: var(--text-primary);
border-radius: 4px;
padding: 15px;
margin: 20px 0;
@ -1184,7 +1363,8 @@ div.message-ribbon {
.monospace-address {
font-family: monospace;
background: #f0f0f0;
background: var(--bg-secondary);
color: var(--text-primary);
padding: 8px;
border-radius: 3px;
word-break: break-all;
@ -1204,6 +1384,15 @@ div.message-ribbon {
cursor: not-allowed;
}
[data-theme="dark"] .disabled-crypto-button {
background-color: #1a1a1a !important;
background-image: none !important;
color: #666666 !important;
border: 2px solid #444444 !important;
cursor: not-allowed;
opacity: 0.6;
}
.crypto-button-icon {
width: 32px;
height: 32px;
@ -1228,8 +1417,9 @@ div.message-ribbon {
}
.warning-banner-alt {
background: #fff3cd;
border: 1px solid #ffc107;
background: color-mix(in srgb, var(--info-color) 15%, var(--bg-primary) 85%);
border: 1px solid var(--info-color);
color: var(--text-primary);
border-radius: 4px;
padding: 15px;
margin: 20px 0;
@ -1249,24 +1439,29 @@ div.message-ribbon {
}
.pending-quote-item {
background: #f8f9fa;
border: 1px solid #dee2e6;
background: transparent;
border: none;
color: var(--text-primary);
border-radius: 3px;
padding: 10px;
margin: 8px 0;
}
.pending-quote-grid {
display: grid;
grid-template-columns: 1fr auto;
align-items: start;
.pending-quote-single {
text-align: center;
}
.pending-quote-status {
color: #666;
margin-left: 10px;
.pending-quote-cancel {
text-align: center;
margin-top: 10px;
}
.full-width-button {
width: 100%;
display: block;
}
.view-quote-button {
background: #17a2b8;
font-size: 12px;
@ -1542,3 +1737,206 @@ div.message-ribbon {
font-size: 0.9em;
font-family: monospace;
}
/* Dark Mode Theme System */
:root {
/* Light mode colors (default) */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #333333;
--text-secondary: #6c757d;
--text-muted: #666666;
--border-color: #dee2e6;
--border-light: #e9ecef;
--link-color: #5f6368;
--blue-color: #98b6fa;
--green-color: #a3c765;
--red-color: #bc2131;
--success-color: #28a745;
--warning-color: #ffc107;
--error-color: #dc3545;
--info-color: #17a2b8;
/* Form elements */
--input-bg: #ffffff;
--input-border: #cccccc;
--input-text: #333333;
/* Navigation */
--nav-bg: #f9f9fa;
--nav-text: #333333;
/* Cards and containers */
--card-bg: #f8f9fa;
--well-bg: #f9f9fa;
/* Status notices */
--notice-warning-bg: #fff3cd;
--notice-warning-border: #ffeaa7;
--notice-error-bg: #f8d7da;
--notice-error-border: #f5c6cb;
--notice-info-bg: #d1ecf1;
--notice-info-border: #bee5eb;
}
[data-theme="dark"] {
/* Dark mode colors - zealotrush.com inspired gaming theme */
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #ffffff;
--text-secondary: #cccccc;
--text-muted: #999999;
--border-color: #7ab9ff;
--border-light: #5599dd;
--link-color: #7ab9ff;
--blue-color: #7ab9ff;
--green-color: #08e700;
--red-color: #F34C31;
--success-color: #08e700;
--warning-color: #f34c31;
--error-color: #f34c31;
--info-color: #7ab9ff;
/* Form elements */
--input-bg: #161b22;
--input-border: #7ab9ff;
--input-text: #ffffff;
/* Navigation */
--nav-bg: #0d1117;
--nav-text: #ffffff;
/* Cards and containers */
--card-bg: #2d2d2d;
--well-bg: #2d2d2d;
/* Status notices */
--notice-warning-bg: #3e2723;
--notice-warning-border: #5d4037;
--notice-error-bg: #3f1a1a;
--notice-error-border: #5c2626;
--notice-info-bg: #1a365d;
--notice-info-border: #2c5282;
}
/* Apply theme variables to existing elements */
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
section.content {
background-color: var(--bg-primary);
color: var(--text-primary);
}
section.nav {
background-color: var(--nav-bg);
color: var(--nav-text);
}
a {
color: var(--link-color);
}
input[type="text"],
input[type="tel"],
input[type="password"],
input[type="number"],
input[type="date"],
textarea {
background-color: var(--input-bg);
border-color: var(--input-border);
color: var(--input-text);
}
.well, .well2 {
background-color: var(--well-bg);
color: var(--text-primary);
}
[data-theme="dark"] .well,
[data-theme="dark"] .well2 {
background-color: #2d3748;
background-image: url("/static/img/trans-blue.png");
border: 2px solid #7ab9ff;
box-shadow: 0 0 10px rgba(122, 185, 255, 0.3);
color: var(--text-primary);
}
/* Dark mode specific overrides for existing classes */
[data-theme="dark"] table tr:nth-child(even) {
background-color: #2d2d2d;
}
[data-theme="dark"] table tr:nth-child(odd) {
background-color: #333333;
}
[data-theme="dark"] .crypto-payment-card {
background: var(--card-bg);
border-color: var(--border-color);
color: var(--text-primary);
}
[data-theme="dark"] .status-warning-notice {
background: var(--notice-warning-bg);
border-color: var(--notice-warning-border);
color: var(--text-primary);
}
[data-theme="dark"] .status-error-notice {
background: var(--notice-error-bg);
border-color: var(--notice-error-border);
color: var(--text-primary);
}
[data-theme="dark"] .status-info-notice {
background: var(--notice-info-bg);
border-color: var(--notice-info-border);
color: var(--text-primary);
}
[data-theme="dark"] .crypto-help-section {
background: var(--bg-tertiary);
color: var(--text-primary);
}
[data-theme="dark"] .invoice-payment-box {
background-color: #1a365d;
border-color: #2c5282;
color: var(--text-primary);
}
/* Crypto wallet success indicators */
.success-indicator {
color: var(--success-color);
font-weight: bold;
}
[data-theme="dark"] .success-indicator {
color: var(--green-color);
font-weight: bold;
}
/* Theme toggle button */
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease;
}
.theme-toggle:hover {
background-color: var(--bg-secondary);
}
.theme-toggle-icon {
margin-right: 5px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

View file

@ -10,28 +10,28 @@
<br/>
<br/>
<a href="/c/new" class="shop-new-button mps-button">&#65291; New Content</a>
<a href="/c/new" class="mps-button shop-new-button">&#65291; New Content</a>
{% if request.user in request.shop.owners %}
<br/>
<br/>
<a href="/coupon/new" class="shop-new-button mps-button">&#65291; New Coupon</a>
<a href="/coupon/new" class="mps-button shop-new-button">&#65291; New Coupon</a>
{% endif %}
<br/>
<br/>
<a href="/p/new" class="shop-new-button mps-button">&#65291; New Product</a>
<a href="/p/new" class="mps-button shop-new-button">&#65291; New Product</a>
{% if request.user in request.shop.owners %}
<br/>
<br/>
<a href="/s/locations/new" class="shop-new-button mps-button">&#65291; New Shop Location</a>
<a href="/s/locations/new" class="mps-button shop-new-button">&#65291; New Shop Location</a>
{% endif %}
{% if request.is_saas_domain %}
<br />
<br />
<a href="/s/new" class="shop-new-button mps-button mps-button">&#65291; New Shop</a>
<a href="/s/new" class="mps-button shop-new-button">&#65291; New Shop</a>
{% endif %}
{% else %}
@ -39,7 +39,7 @@
{% if request.is_saas_domain %}
<p>Create your first shop to get started!</p>
<br/>
<a href="/s/new" class="shop-new-button mps-button">&#65291; Create Your First Shop</a>
<a href="/s/new" class="mps-button shop-new-button">&#65291; Create Your First Shop</a>
{% else %}
<p>No shop configured for this domain.</p>
{% endif %}

View file

@ -10,38 +10,38 @@
<br/>
<br/>
<a href="/s/{{ request.shop.id }}/{{ request.shop.slug }}" class="product-edit-button mps-button">&nbsp View Shop</a>
<a href="/s/{{ request.shop.id }}/{{ request.shop.slug }}" class="mps-button product-edit-button">&nbsp View Shop</a>
<br/>
<br/>
<a href="/s/{{ request.shop.id }}/products" class="product-edit-button mps-button">&nbsp View Products</a>
<a href="/s/{{ request.shop.id }}/products" class="mps-button product-edit-button">&nbsp View Products</a>
{% if request.user in request.shop.owners %}
<br/>
<br/>
<a href="/s/{{ request.shop.id }}/coupons" class="product-edit-button mps-button">&nbsp View Coupons</a>
<a href="/s/{{ request.shop.id }}/coupons" class="mps-button product-edit-button">&nbsp View Coupons</a>
<br/>
<br/>
<a href="/s/{{ request.shop.id }}/users" class="product-edit-button mps-button">&#128100; &nbsp Shop Users</a>
<a href="/s/{{ request.shop.id }}/users" class="mps-button product-edit-button">&#128100; &nbsp Shop Users</a>
<br/>
<br/>
<a href="/s/{{ request.shop.id }}/sales" class="product-edit-button mps-button">💰 &nbsp Shop Sales</a>
<a href="/s/{{ request.shop.id }}/sales" class="mps-button product-edit-button">💰 &nbsp Shop Sales</a>
<br/>
<br/>
<a href="/s/{{ request.shop.id }}/settings" class="product-edit-button mps-button">&#9881; &nbsp Shop Settings</a>
<a href="/s/{{ request.shop.id }}/settings" class="mps-button product-edit-button">&#9881; &nbsp Shop Settings</a>
{% endif %}
<br/>
<br/>
<a href="/s/locations" class="product-edit-button mps-button">&#127968; &nbsp Shop Locations</a>
<a href="/s/locations" class="mps-button product-edit-button">&#127968; &nbsp Shop Locations</a>
{% if request.is_saas_domain %}
<br/>
<br/>
<a href="/u/shops" class="product-edit-button mps-button">&#9974; &nbsp; Switch Shop</a>
<a href="/u/shops" class="mps-button product-edit-button">&#9974; &nbsp; Switch Shop</a>
{% endif %}
{% endif %}

View file

@ -7,11 +7,53 @@
<!--product_created={{ request.product.human_created_timestamp }}-->
<!--product_updated={{ request.product.human_updated_timestamp }}-->
{%- endif %}
<html lang="en" data-theme="{{ request.html_data_theme or 'dark' }}">
{% set user_theme_id = request.user.theme_id if request.user and request.user.authenticated else none %}
{% set shop_default_theme = request.shop.default_theme if request.shop else 1 %}
{% if user_theme_id is not none %}
{% set theme_preference = 'light' if (user_theme_id == 1) else 'dark' %}
{% else %}
{% set theme_preference = 'light' if (shop_default_theme == 1) else 'dark' %}
{% endif %}
<html lang="en" data-theme="{{ theme_preference }}" data-user-theme="{{ user_theme_id or 'null' }}" data-shop-default="{{ shop_default_theme }}">
<head>
<!-- stylesheet to-end-all stylesheets, pico.fluid.classless.css ! woot -->
<link rel="stylesheet" href="/static/css/common.css">
<!-- Dark mode theme switching -->
<script>
(function() {
// Theme priority: localStorage > user preference > shop default > system default (light)
const userThemeId = {{ user_theme_id or 'null' }};
const shopDefaultTheme = {{ shop_default_theme }};
// Get theme from localStorage
const savedTheme = localStorage.getItem('theme-preference');
let finalTheme;
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
// User has explicitly chosen a theme via toggle
finalTheme = savedTheme;
} else if (userThemeId !== null) {
// Use user's profile setting (0=dark, 1=light)
finalTheme = userThemeId === 0 ? 'dark' : 'light';
} else {
// Use shop default (0=dark, 1=light)
finalTheme = shopDefaultTheme === 0 ? 'dark' : 'light';
}
// Apply theme immediately to prevent flash
document.documentElement.setAttribute('data-theme', finalTheme);
// Theme switching function (for programmatic use)
window.setTheme = function(theme) {
if (theme === 'dark' || theme === 'light') {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme-preference', theme);
}
};
})();
</script>
{% include 'snippets/analytics.j2' %}

View file

@ -9,9 +9,14 @@
{% block content -%}
{% if cart.is_empty %}
<section class="one-column">
<section class="well">
{% else %}
<section class="cart-grid">
<section class="cart-left">
{% endif %}
{% for coupon in cart.coupons %}
<section class="coupon">
@ -64,15 +69,16 @@
<center>
<h2>This cart is empty.</h2>
Nothing to see here.
{% if request.user and request.user.authenticated and request.user.carts.count() > 1 %}
<br/>
<br/>
<a href="/u/carts" class="product-edit-button mps-button mps-button-small">View Saved Carts</a>
<br/>
<br/> or
<br/>
<br/>
{% endif %}
<br/>
<br/>
<a href="/" class="cart-checkout-button mps-button mps-button-small mps-button-green">Let's go shopping!</a>
</center>
@ -185,6 +191,7 @@
</section>
{% if not cart.is_empty %}
<section class="cart-right well">
{% if cart.is_discounted %}
@ -275,6 +282,7 @@
</center>
</section>
{% endif %}
</section>

View file

@ -42,8 +42,7 @@
<br/>
{% if cart.requires_payment %}
Are you sure you want to confirm checkout & pay <br/>
<b>${{ '{:,.2f}'.format(cart.total) }}</b>?
Are you sure you want to confirm checkout & pay <b>${{ '{:,.2f}'.format(cart.total) }}</b>?
{% else %}
Are you sure you want to confirm checkout?
{% endif %}
@ -61,7 +60,7 @@
{% if monero_enabled and xmr_processor_enabled and cart.requires_payment and cart.shop_product_dict|length == 1 %}
<br/>
{% if pending_crypto_quotes %}
<button type="button" class="mps-button disabled-crypto-button" disabled title="Complete or cancel your pending crypto payments first"><img src="{{ request.static_url('make_post_sell:static/monero-symbol-480.png') }}" class="crypto-button-icon">Monero (XMR) - Complete Pending Payments First</button>
<button type="button" class="mps-button disabled-crypto-button" disabled title="Complete or cancel your pending crypto payments first"><img src="{{ request.static_url('make_post_sell:static/monero-symbol-480.png') }}" class="crypto-button-icon">Monero (XMR) - Complete or Cancel Pending Payment</button>
{% else %}
<form method="POST" action="{{ request.route_url('crypto_xmr_start') }}">
{% include "snippets/csrf.j2" %}
@ -80,7 +79,7 @@
{% if dogecoin_enabled and doge_processor_enabled and cart.requires_payment and cart.shop_product_dict|length == 1 %}
<br/>
{% if pending_crypto_quotes %}
<button type="button" class="mps-button disabled-crypto-button" disabled title="Complete or cancel your pending crypto payments first"><img src="{{ request.static_url('make_post_sell:static/dogecoin-logo.png') }}" class="crypto-button-icon">Dogecoin (DOGE) - Complete Pending Payments First</button>
<button type="button" class="mps-button disabled-crypto-button" disabled title="Complete or cancel your pending crypto payments first"><img src="{{ request.static_url('make_post_sell:static/dogecoin-logo.png') }}" class="crypto-button-icon">Dogecoin (DOGE) - Complete or Cancel Pending Payment</button>
{% else %}
<form method="POST" action="{{ request.route_url('crypto_doge_start') }}">
{% include "snippets/csrf.j2" %}
@ -124,27 +123,18 @@
{# Show pending crypto quotes if user has any #}
{% if pending_crypto_quotes %}
<div class="warning-banner-alt">
<h3>📋 {{ pending_crypto_quotes|length }} Pending Crypto Quote{{ 's' if pending_crypto_quotes|length != 1 else '' }}</h3>
<p class="warning-banner-content">You have {{ pending_crypto_quotes|length }} pending crypto payment{{ 's' if pending_crypto_quotes|length != 1 else '' }}:</p>
<h3>📋 Pending {{ pending_crypto_quotes[0].coin_type }} Payment</h3>
{% for quote in pending_crypto_quotes %}
<div class="pending-quote-item">
<div class="pending-quote-grid">
<div>
<strong>{{ quote.coin_type }}</strong>
<span class="pending-quote-status">
{{ quote.status.title() }}
{% if quote.current_confirmations > 0 %}
({{ quote.confirmation_status }})
{% endif %}
</span>
</div>
<div>
<a href="{{ request.route_url('crypto_quote', payment_id=quote.id) }}" class="mps-button view-quote-button">View Quote</a>
</div>
</div>
<div class="pending-quote-details">
Amount: {{ '%.8f' % (quote.expected_amount / (1000000000000 if quote.coin_type == 'XMR' else 100000000)) }} {{ quote.coin_type }}
</div>
<div class="pending-quote-single">
<a href="{{ request.route_url('crypto_quote', payment_id=quote.id) }}" class="mps-button mps-button-blue full-width-button">View Quote</a>
</div>
<div class="pending-quote-cancel">
<button onclick="cancelQuote('{{ quote.id }}')" class="mps-button mps-button-red payment-button-cancel full-width-button">Cancel</button>
</div>
</div>
{% endfor %}
</div>
@ -169,8 +159,11 @@ function cancelQuote(paymentId) {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Payment quote cancelled successfully');
window.location.reload();
if (data.cart_url) {
window.location.href = data.cart_url;
} else {
window.location.reload();
}
} else {
alert('Failed to cancel quote: ' + (data.message || 'Unknown error'));
}

View file

@ -163,9 +163,6 @@
/>
<br />
<br />
</div>
<br />
{% if request.shop.stripe_enabled %}
@ -173,9 +170,12 @@
<input type="submit" name="disable_stripe" class="payment-toggle-button disable" value="Disable Stripe" />
{% else %}
<input type="submit" name="submit" class="payment-toggle-button enable" value="Re-enable Stripe" />
<small class="status-message">Enter your API keys to re-enable Stripe payments</small>
{% endif %}
</div>
<br />
<br />
<br />
@ -504,6 +504,16 @@
<br />
<br />
<label for="default_theme_input">Default Theme for Visitors</label>
<select name="default_theme" id="default_theme_input" class="full-width-input">
<option value="1" {% if default_theme == 1 %}selected{% endif %}>Light Mode</option>
<option value="0" {% if default_theme == 0 %}selected{% endif %}>Dark Mode</option>
</select>
<small class="note-text">Choose the default theme that new visitors see. Users can override this in their profile settings.</small>
<br />
<br />
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
<br />

View file

@ -28,32 +28,16 @@
placeholder = "Your {{ coin }} address for automatic refunds"
/>
<br />
<label for="{{ coin.lower() }}_label_input">Label (Optional)</label>
<input
name = "label"
type = "text"
id = "{{ coin.lower() }}_label_input"
class = "mps-crypto-label full-width-input"
value = "{% if addresses.get(coin) %}{{ addresses[coin].label }}{% endif %}"
placeholder = "e.g., My Hardware Wallet"
/>
<br />
<br />
{% if addresses.get(coin) %}
<input type="submit" name="submit" class="mps-submit" value="Update {{ coin }} Address" />
{% else %}
<input type="submit" name="submit" class="mps-submit" value="Save {{ coin }} Address" />
{% endif %}
<input type="submit" name="submit" class="mps-submit" value="Save {{ coin }} Refund Address" />
<br />
<br />
</form>
<hr />
{% if not loop.last %}<hr />{% endif %}
{% endfor %}
{% else %}
<p>No cryptocurrencies are currently enabled on this platform.</p>

View file

@ -35,19 +35,8 @@
placeholder = "Full Name"
required />
<!-- not implemented.
<fieldset>
<legend>User Theme</legend>
<input type="radio" name="user_theme" id="light_mode" value="1" {% if user_theme == 1 %}checked{% endif %} />
<label for="user_theme" class="inline-label">Light mode</label>
<input type="radio" name="user_theme" id="visibility_unlisted" value="0" {% if user_theme == 0 %}checked{% endif %} />
<label for="user_theme" class="inline-label">Dark mode</label>
</fieldset>
-->
<br />
<br />
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
@ -56,6 +45,53 @@
</form>
</section>
</section>
<br />
<section class="one-column">
<section class="well">
<h3>Theme Preference</h3>
<form method="post" action="/u/settings" onsubmit="submit.disabled = true; return true;">
<input type="radio" name="theme_id" id="light_mode" value="1" {% if theme_id == 1 %}checked{% endif %} />
<label for="light_mode" class="inline-label">Light Mode</label>
<br />
<input type="radio" name="theme_id" id="dark_mode" value="0" {% if theme_id == 0 %}checked{% endif %} />
<label for="dark_mode" class="inline-label">Dark Mode</label>
</form>
<script>
// Handle theme preference form specifically
document.addEventListener('DOMContentLoaded', function() {
const themeForm = document.querySelector('form:has(input[name="theme_id"])');
const themeRadios = document.querySelectorAll('input[name="theme_id"]');
if (themeForm && themeRadios.length > 0) {
themeForm.addEventListener('submit', function() {
// Set localStorage to match the selected theme preference
const selectedRadio = document.querySelector('input[name="theme_id"]:checked');
if (selectedRadio) {
const newTheme = selectedRadio.value === '0' ? 'dark' : 'light';
localStorage.setItem('theme-preference', newTheme);
}
});
// Also update localStorage immediately when radio button changes
themeRadios.forEach(function(radio) {
radio.addEventListener('change', function() {
// Apply new theme immediately
const newTheme = this.value === '0' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
// Set localStorage to match user preference so it persists across pages
localStorage.setItem('theme-preference', newTheme);
});
});
}
});
</script>
</section>
</section>
<br />

View file

@ -382,6 +382,9 @@ def shop_settings(request):
ribbon_color_1 = request.params.get("ribbon_color_1", shop.ribbon_color_1).strip()
ribbon_color_2 = request.params.get("ribbon_color_2", shop.ribbon_color_2).strip()
# Theme settings
default_theme = request.params.get("default_theme", shop.default_theme)
google_analytics_id = request.params.get(
"google_analytics_id", shop.google_analytics_id
)
@ -551,6 +554,16 @@ def shop_settings(request):
)
request.session.flash(msg)
# Handle default theme setting
if default_theme and int(default_theme) != shop.default_theme:
shop.default_theme = int(default_theme)
theme_name = "Dark Mode" if int(default_theme) == 0 else "Light Mode"
msg = (
f"Shop default theme set to {theme_name}.",
"success",
)
request.session.flash(msg)
# Handle stripe settings form
if form_section == "stripe-settings":
# Handle disable action
@ -877,6 +890,7 @@ def shop_settings(request):
"ribbon_text_color": shop.ribbon_text_color or "",
"ribbon_color_1": shop.ribbon_color_1 or "",
"ribbon_color_2": shop.ribbon_color_2 or "",
"default_theme": shop.default_theme,
"google_analytics_id": shop.google_analytics_id or "",
"plausible_domain_name": shop.plausible_domain_name or "",
"stripe_public_api_key": shop.stripe_public_api_key or "",

View file

@ -50,6 +50,7 @@ def user_settings(request):
name = request.params.get("name", user.name).strip()
full_name = request.params.get("full_name", user.full_name or "").strip()
theme_id = request.params.get("theme_id", user.theme_id)
if name and name != user.name:
if not is_user_name_valid(name):
@ -74,9 +75,17 @@ def user_settings(request):
msg = ("You set your private full name.", "success")
request.session.flash(msg)
# Handle theme preference changes
if theme_id and int(theme_id) != user.theme_id:
user.theme_id = int(theme_id)
theme_name = "Dark Mode" if int(theme_id) == 0 else "Light Mode"
msg = (f"Theme preference set to {theme_name}.", "success")
request.session.flash(msg)
return {
"name": name,
"full_name": full_name,
"theme_id": user.theme_id,
}