Implement comprehensive mobile experience framework for entire application

This commit is contained in:
Eric Gullickson
2025-07-27 21:03:06 -05:00
parent f46d471453
commit ea055f1c38
20 changed files with 713 additions and 174 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,48 @@
---
name: k8s-deployment-architect
description: Use this agent when you need to design, review, or optimize Kubernetes deployments with a focus on high availability, maintainability, and GitOps automation. Examples: <example>Context: User is working on containerizing their MotoVaultPro application for Kubernetes deployment. user: 'I need to create Kubernetes manifests for my ASP.NET Core application that can be deployed with FluxCD' assistant: 'I'll use the k8s-deployment-architect agent to design production-ready Kubernetes manifests with proper high availability patterns and FluxCD integration.' <commentary>Since the user needs Kubernetes deployment architecture, use the k8s-deployment-architect agent to create maintainable, HA-focused manifests.</commentary></example> <example>Context: User wants to review existing Kubernetes configurations for best practices. user: 'Can you review my deployment.yaml and suggest improvements for better availability and maintainability?' assistant: 'Let me use the k8s-deployment-architect agent to analyze your Kubernetes configuration and provide recommendations for high availability and maintainability improvements.' <commentary>The user is asking for review of Kubernetes configurations, which is exactly what the k8s-deployment-architect agent specializes in.</commentary></example>
color: purple
---
You are an expert Kubernetes architect specializing in highly available, production-ready deployments with GitOps automation. Your expertise encompasses container orchestration, high availability patterns, and maintainable infrastructure-as-code practices.
Your primary responsibilities:
**High Availability Design:**
- Design multi-replica deployments with proper anti-affinity rules
- Implement health checks, readiness probes, and liveness probes
- Configure resource limits, requests, and horizontal pod autoscaling
- Design for graceful shutdowns and zero-downtime deployments
- Implement circuit breakers and retry mechanisms where appropriate
**Maintainability Focus:**
- Create clear, well-documented YAML manifests with extensive comments
- Use consistent naming conventions and labeling strategies
- Organize configurations into logical, modular files
- Implement configuration management through ConfigMaps and Secrets
- Design for easy debugging and troubleshooting by new team members
**FluxCD Integration:**
- Structure manifests for GitOps workflows with proper directory organization
- Implement Kustomization files for environment-specific configurations
- Design for automated deployments with proper dependency management
- Include monitoring and alerting configurations for deployment health
- Ensure configurations support rollback and canary deployment strategies
**Code Quality Standards:**
- Follow Kubernetes best practices and security guidelines
- Implement proper RBAC and security contexts
- Use semantic versioning for container images
- Include comprehensive documentation within manifests
- Design for testability and validation
**Onboarding Considerations:**
- Create README files explaining the deployment architecture
- Include troubleshooting guides and common operational procedures
- Document environment-specific configurations and requirements
- Provide clear examples and templates for common tasks
- Implement logging and monitoring that aids in understanding system behavior
When reviewing existing configurations, provide specific, actionable recommendations with explanations of why each change improves availability or maintainability. When creating new configurations, start with the overall architecture and then provide detailed, production-ready manifests.
Always consider the operational burden on new team members and design solutions that are self-documenting and follow established patterns. Prioritize reliability and clarity over complexity.

View File

@@ -38,7 +38,6 @@ jobs:
ericgullickson/motovaultpro ericgullickson/motovaultpro
ghcr.io/ericgullickson/motovaultpro ghcr.io/ericgullickson/motovaultpro
tags: | tags: |
type=edge,branch=main
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag type=ref,event=tag

View File

@@ -24,9 +24,9 @@ namespace MotoVaultPro.Helper
public const string GenericErrorMessage = "An error occurred, please try again later"; public const string GenericErrorMessage = "An error occurred, please try again later";
public const string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt"; public const string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt";
public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx"; public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public const string SponsorsPath = "https://fbtech.github.io/fbtech/sponsors.json"; public const string SponsorsPath = "https://ericgullickson.github.io/ericgullickson/sponsors.json";
public const string TranslationPath = "https://fbtech.github.io/motovaultpro_translations"; public const string TranslationPath = "https://ericgullickson.github.io/motovaultpro_translations";
public const string ReleasePath = "https://api.github.com/repos/fbtech/motovaultpro/releases/latest"; public const string ReleasePath = "https://api.github.com/repos/ericgullickson/motovaultpro/releases/latest";
public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json"; public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
public const string ReportNote = "Report generated by MotoVaultPro, a Free and Open Source Vehicle Maintenance Tracker - MotoVaultPro.com"; public const string ReportNote = "Report generated by MotoVaultPro, a Free and Open Source Vehicle Maintenance Tracker - MotoVaultPro.com";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input) public static string GetTitleCaseReminderUrgency(ReminderUrgency input)
@@ -315,7 +315,7 @@ namespace MotoVaultPro.Helper
Console.WriteLine($"MotoVaultPro {VersionNumber}"); Console.WriteLine($"MotoVaultPro {VersionNumber}");
Console.WriteLine("Website: https://motovaultpro.com"); Console.WriteLine("Website: https://motovaultpro.com");
Console.WriteLine("Documentation: https://docs.motovaultpro.com"); Console.WriteLine("Documentation: https://docs.motovaultpro.com");
Console.WriteLine("GitHub: https://github.com/fbtech/motovaultpro"); Console.WriteLine("GitHub: https://github.com/ericgullickson/motovaultpro");
var mailConfig = config.GetSection("MailConfig").Get<MailConfig>(); var mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer)) if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{ {

185
MOBILE.md Normal file
View File

@@ -0,0 +1,185 @@
# Mobile Experience Improvement Plan for Add Fuel Record Screen
## Analysis Summary
The current add fuel record screen has significant mobile UX issues that create pain points for users on mobile devices. The interface feels like a shrunken desktop version rather than a mobile-first experience.
## Critical Mobile UX Issues Identified
### 1. Modal Size and Viewport Problems
- Uses Bootstrap's default modal sizing without mobile optimization
- No mobile-specific modal sizing classes or responsive adjustments
- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml`
### 2. Touch Target Size Issues
- Small "+" button for odometer increment (44px minimum not met)
- Small close button in header
- Form switch toggles too small for reliable touch interaction
- **File Locations**:
- `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 69, 99, 51, 48, 106, 110)
### 3. Dense Two-Column Layout Problems
- Advanced mode uses `col-md-6` layout creating cramped display
- Fields become too narrow for comfortable text input
- Second column with file upload becomes nearly unusable
- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 59, 139)
### 4. Complex Header Layout on Mobile
- Modal header contains multiple elements in cramped flex layout
- Toggle labels may wrap or get cut off
- Mode switch becomes hard to understand and use
- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 44-53)
### 5. Input Field Accessibility Issues
- Decimal inputs with custom key interceptors interfere with mobile keyboards
- Multi-select dropdown for tags difficult on mobile
- File upload interface unusable in narrow mobile view
- **File Locations**:
- `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 74, 103, 117, 127, 130-135)
- `/wwwroot/js/gasrecord.js`
### 6. Modal Footer Button Layout
- Multiple buttons including conditional "Delete" button create touch conflicts
- Risk of accidental deletion or difficulty reaching primary action
- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` (line 155)
### 7. Form Mode Switching UX
- Simple/Advanced mode toggle jarring on mobile
- Content suddenly appears/disappears
- Users might not understand mode switching capability
- **File Location**: `/wwwroot/js/gasrecord.js` (lines 509-536)
### 8. Keyboard and Input Mode Issues
- Mixed input types with custom JavaScript key handlers
- Mobile keyboards may not behave predictably
- **File Locations**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/js/gasrecord.js`
### 9. Date Picker Mobile Issues
- Bootstrap datepicker doesn't provide optimal mobile experience
- Native mobile date pickers would be better
- **File Location**: `/wwwroot/js/gasrecord.js` (lines 6, 29)
### 10. No Progressive Enhancement for Mobile
- No mobile-specific CSS classes or touch-friendly spacing
- No mobile-optimized layouts
- **File Locations**:
- `/wwwroot/css/site.css`
- `/Views/Vehicle/Gas/_GasModal.cshtml`
## Mobile Experience Improvement Plan
### Priority 1: Critical Mobile UX Fixes
#### 1. Mobile-First Modal Design
- Implement full-screen modal on mobile devices
- Add slide-up animation for native app feel
- Create mobile-specific modal header with simplified layout
- **Files to Modify**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/css/site.css`
- `/wwwroot/js/gasrecord.js`
#### 2. Touch Target Optimization
- Increase all interactive elements to minimum 44px
- Add larger padding around buttons and form controls
- Implement touch-friendly spacing between elements
- **Files to Modify**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/css/site.css`
#### 3. Single-Column Mobile Layout
- Force single-column layout on mobile regardless of mode
- Stack all form fields vertically with proper spacing
- Move file upload and notes to dedicated sections
- **Files to Modify**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/css/site.css`
### Priority 2: Input and Interaction Improvements
#### 4. Mobile-Optimized Inputs
- Replace Bootstrap datepicker with native HTML5 date input on mobile
- Simplify tag selection with mobile-friendly chip input
- Improve number input keyboards with proper `inputmode` attributes
- **Files to Modify**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/js/gasrecord.js`
#### 5. Form Mode Simplification
- Default to Simple mode on mobile
- Make mode toggle more prominent and clear
- Add smooth transitions between modes
- **Files to Modify**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/js/gasrecord.js`
- `/Controllers/Vehicle/GasController.cs`
### Priority 3: Enhanced Mobile Features
#### 6. Bottom Sheet Pattern
- Implement native-style bottom sheet for mobile
- Add swipe-to-dismiss gesture
- Include pull handle for better UX
- **Files to Modify**:
- `/Views/Vehicle/Gas/_GasModal.cshtml`
- `/wwwroot/css/site.css`
- `/wwwroot/js/gasrecord.js`
#### 7. Mobile-Specific CSS Improvements
- Add mobile breakpoint styles
- Implement proper touch feedback
- Optimize form field sizing for mobile keyboards
- **Files to Modify**:
- `/wwwroot/css/site.css`
#### 8. Progressive Enhancement
- Add mobile detection for conditional features
- Implement haptic feedback where supported
- Add mobile-specific validation styling
- **Files to Modify**:
- `/wwwroot/js/gasrecord.js`
- `/wwwroot/js/shared.js`
- `/Views/Shared/_Layout.cshtml`
## Implementation Strategy
### Phase 1: Modal and Layout Fixes (Priority 1 items)
- Focus on making the most impactful changes first
- Ensure mobile modal feels native and intuitive
- Implement proper touch targets and single-column layout
### Phase 2: Input Optimizations (Priority 2 items)
- Optimize form inputs for mobile interaction
- Simplify complex form elements
- Improve mode switching experience
### Phase 3: Advanced Mobile Features (Priority 3 items)
- Add sophisticated mobile interaction patterns
- Implement progressive enhancement
- Add mobile-specific features and feedback
## Key Files for Mobile Improvements
### Primary Files:
- `/Views/Vehicle/Gas/_GasModal.cshtml` - Main modal template
- `/wwwroot/js/gasrecord.js` - Modal behavior and form handling
- `/wwwroot/css/site.css` - Styling and responsive design
### Supporting Files:
- `/Controllers/Vehicle/GasController.cs` - Server-side logic
- `/Views/Shared/_Layout.cshtml` - Global mobile configuration
- `/wwwroot/js/shared.js` - Shared JavaScript utilities
## Success Metrics
- Touch target compliance (minimum 44px)
- Single-column layout on mobile breakpoints
- Native mobile input patterns
- Improved task completion rates on mobile
- Reduced user friction and abandonment
## Notes
This plan maintains existing functionality while transforming the mobile experience from a desktop-centric interface to a mobile-first, touch-optimized experience that feels native and intuitive on mobile devices.

View File

@@ -15,7 +15,7 @@
<form> <form>
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="collisionRecordDate">@translator.Translate(userLanguage, "Date")</label> <label for="collisionRecordDate">@translator.Translate(userLanguage, "Date")</label>
<div class="input-group"> <div class="input-group">
@@ -54,7 +54,7 @@
</select> </select>
@await Html.PartialAsync("_ExtraField", Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<label for="collisionRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="collisionRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="collisionRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="collisionRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (isNew) @if (isNew)

View File

@@ -15,7 +15,7 @@
<form> <form>
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-md-6 col-12" id="reminderOptions"> <div class="col-md-6 col-12 mobile-single-column" id="reminderOptions">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="reminderDescription">@translator.Translate(userLanguage,"Description")</label> <label for="reminderDescription">@translator.Translate(userLanguage,"Description")</label>
<input type="text" id="reminderDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Reminder Description")" value="@Model.Description"> <input type="text" id="reminderDescription" class="form-control" placeholder="@translator.Translate(userLanguage,"Reminder Description")" value="@Model.Description">
@@ -54,7 +54,7 @@
} }
</select> </select>
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<label for="reminderNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="reminderNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea>
<div class="form-check form-switch"> <div class="form-check form-switch">

View File

@@ -15,7 +15,7 @@
<form> <form>
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="serviceRecordDate">@translator.Translate(userLanguage,"Date")</label> <label for="serviceRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
@@ -54,7 +54,7 @@
</select> </select>
@await Html.PartialAsync("_ExtraField", Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<label for="serviceRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="serviceRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="serviceRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="serviceRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (isNew) @if (isNew)

View File

@@ -15,7 +15,7 @@
<form> <form>
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="taxRecordDate">@translator.Translate(userLanguage,"Date")</label> <label for="taxRecordDate">@translator.Translate(userLanguage,"Date")</label>
<div class="input-group"> <div class="input-group">
@@ -43,7 +43,7 @@
</select> </select>
@await Html.PartialAsync("_ExtraField", Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<label for="taxRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="taxRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="taxRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="taxRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
<div class="form-check form-switch"> <div class="form-check form-switch">

View File

@@ -15,7 +15,7 @@
<form> <form>
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="upgradeRecordDate">@translator.Translate(userLanguage, "Date")</label> <label for="upgradeRecordDate">@translator.Translate(userLanguage, "Date")</label>
<div class="input-group"> <div class="input-group">
@@ -54,7 +54,7 @@
</select> </select>
@await Html.PartialAsync("_ExtraField", Model.ExtraFields) @await Html.PartialAsync("_ExtraField", Model.ExtraFields)
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12 mobile-single-column">
<label for="upgradeRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label> <label for="upgradeRecordNotes">@translator.Translate(userLanguage, "Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>
<textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea> <textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (isNew) @if (isNew)

View File

@@ -27,7 +27,7 @@
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6 mobile-single-column">
<label for="inputYear">@translator.Translate(userLanguage, "Year")</label> <label for="inputYear">@translator.Translate(userLanguage, "Year")</label>
<input type="number" inputmode="numeric" id="inputYear" class="form-control" placeholder="@translator.Translate(userLanguage, "Year(must be after 1900)")" value="@(isNew ? "" : Model.Year)"> <input type="number" inputmode="numeric" id="inputYear" class="form-control" placeholder="@translator.Translate(userLanguage, "Year(must be after 1900)")" value="@(isNew ? "" : Model.Year)">
<label for="inputMake">@translator.Translate(userLanguage, "Make")</label> <label for="inputMake">@translator.Translate(userLanguage, "Make")</label>
@@ -46,7 +46,7 @@
} }
</select> </select>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6 mobile-single-column">
<label for="inputFuelType">@translator.Translate(userLanguage, "Fuel Type")</label> <label for="inputFuelType">@translator.Translate(userLanguage, "Fuel Type")</label>
<select class="form-select" id="inputFuelType")> <select class="form-select" id="inputFuelType")>
<!option value="Gasoline" @(!Model.IsDiesel && !Model.IsElectric ? "selected" : "")>@translator.Translate(userLanguage, "Gasoline")</!option> <!option value="Gasoline" @(!Model.IsDiesel && !Model.IsElectric ? "selected" : "")>@translator.Translate(userLanguage, "Gasoline")</!option>

View File

@@ -378,6 +378,177 @@ html {
.is-valid:focus { .is-valid:focus {
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25) !important; box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25) !important;
} }
/* iPhone-specific optimizations for high DPI displays */
.modal-header .modal-title {
font-size: 1.125rem;
line-height: 1.3;
word-break: break-word;
}
/* Optimize for very narrow screens (iPhone portrait) */
.modal-body .form-group {
margin-bottom: 1.25rem;
}
.modal-body .form-group label {
font-size: 0.9rem;
margin-bottom: 0.5rem;
display: block;
font-weight: 600;
}
/* Enhanced input styling for high DPI */
.form-control,
.form-select {
border-width: 1.5px;
border-radius: 8px;
line-height: 1.4;
}
/* Improved button styling for high DPI touch */
.btn {
border-radius: 8px;
font-weight: 500;
letter-spacing: 0.025em;
}
.btn-sm {
border-radius: 6px;
}
/* Enhanced modal animations for smooth feel */
.modal.fade .modal-dialog {
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Better spacing for very narrow viewports */
.input-group {
margin-bottom: 1rem;
}
.input-group-text {
border-width: 1.5px;
border-radius: 0 8px 8px 0;
}
.input-group .form-control {
border-radius: 8px 0 0 8px;
}
/* Optimize checkbox and radio sizing for high DPI */
.form-check-input {
margin-top: 0.125rem;
border-width: 1.5px;
}
.form-check-label {
font-size: 0.9rem;
padding-left: 0.75rem;
}
/* Enhanced focus rings for high DPI */
.form-control:focus,
.form-select:focus,
.btn:focus {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
border-color: #86b7fe;
}
/* Better modal pull handle for high DPI */
.modal-content::before {
width: 48px;
height: 5px;
border-radius: 3px;
background-color: #adb5bd;
top: 10px;
}
}
/* Ultra-narrow portrait optimization (iPhone-specific) */
@media (max-width: 280px) {
.modal-header {
padding: 0.75rem;
flex-direction: column;
align-items: stretch;
text-align: center;
}
.modal-header .btn-close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 36px;
height: 36px;
}
.modal-header .modal-title {
font-size: 1rem;
margin-bottom: 0.75rem;
padding-right: 50px; /* Account for close button */
}
.modal-body {
padding: 0.75rem;
}
.modal-footer {
padding: 0.75rem;
gap: 0.75rem;
}
/* Ultra-compact form styling */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
font-size: 0.85rem;
margin-bottom: 0.375rem;
}
.form-control,
.form-select {
padding: 0.625rem 0.75rem;
font-size: 15px; /* Prevent zoom on very narrow iPhones */
}
.btn {
padding: 0.625rem 1rem;
font-size: 0.9rem;
}
/* Compact input groups */
.input-group-text {
padding: 0.625rem 0.75rem;
font-size: 0.9rem;
}
/* Smaller modal content spacing */
.modal-content::before {
width: 40px;
height: 4px;
top: 8px;
}
/* Optimize for one-handed use */
.modal-footer .btn {
min-height: 48px;
font-size: 1rem;
}
/* Better tag input styling for narrow screens */
.mobile-tag-input .bootstrap-tagsinput {
min-height: 40px;
padding: 6px;
font-size: 15px;
}
.mobile-tag-input .tag {
font-size: 0.8rem;
padding: 3px 6px;
margin: 1px;
}
} }
.recordSticker { .recordSticker {

View File

@@ -1,10 +1,26 @@
// Initialize collision record modal for mobile (using shared mobile framework)
function initializeCollisionRecordMobile() {
initMobileModal({
modalId: '#collisionRecordModal',
dateInputId: '#collisionRecordDate',
tagSelectorId: '#collisionRecordTag'
});
// Handle desktop initialization
if (!isMobileDevice()) {
initDatePicker($('#collisionRecordDate'));
initTagSelector($("#collisionRecordTag"));
}
}
function showAddCollisionRecordModal() { function showAddCollisionRecordModal() {
$.get('/Vehicle/GetAddCollisionRecordPartialView', function (data) { $.get('/Vehicle/GetAddCollisionRecordPartialView', function (data) {
if (data) { if (data) {
$("#collisionRecordModalContent").html(data); $("#collisionRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#collisionRecordDate')); // Initialize mobile experience using shared framework
initTagSelector($("#collisionRecordTag")); initializeCollisionRecordMobile();
$('#collisionRecordModal').modal('show'); $('#collisionRecordModal').modal('show');
} }
}); });
@@ -25,9 +41,8 @@ function showEditCollisionRecordModal(collisionRecordId, nocache) {
$.get(`/Vehicle/GetCollisionRecordForEditById?collisionRecordId=${collisionRecordId}`, function (data) { $.get(`/Vehicle/GetCollisionRecordForEditById?collisionRecordId=${collisionRecordId}`, function (data) {
if (data) { if (data) {
$("#collisionRecordModalContent").html(data); $("#collisionRecordModalContent").html(data);
//initiate datepicker // Initialize mobile experience using shared framework
initDatePicker($('#collisionRecordDate')); initializeCollisionRecordMobile();
initTagSelector($("#collisionRecordTag"));
$('#collisionRecordModal').modal('show'); $('#collisionRecordModal').modal('show');
bindModalInputChanges('collisionRecordModal'); bindModalInputChanges('collisionRecordModal');
$('#collisionRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { $('#collisionRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {

View File

@@ -1,11 +1,33 @@
// Initialize vehicle modal for mobile (using shared mobile framework)
function initializeVehicleMobile() {
// Vehicle modal has multiple date inputs, handle them individually
if (isMobileDevice()) {
// Convert date inputs to native HTML5 on mobile
$('#inputPurchaseDate').attr('type', 'date').removeClass('datepicker');
$('#inputSoldDate').attr('type', 'date').removeClass('datepicker');
// Initialize mobile tag selector
initMobileTagSelector($("#inputTag"));
// Initialize swipe to dismiss
initSwipeToDismiss('#addVehicleModal');
} else {
// Desktop initialization
initTagSelector($("#inputTag"));
initDatePicker($('#inputPurchaseDate'));
initDatePicker($('#inputSoldDate'));
}
}
function showAddVehicleModal() { function showAddVehicleModal() {
uploadedFile = ""; uploadedFile = "";
$.get('/Vehicle/AddVehiclePartialView', function (data) { $.get('/Vehicle/AddVehiclePartialView', function (data) {
if (data) { if (data) {
$("#addVehicleModalContent").html(data); $("#addVehicleModalContent").html(data);
initTagSelector($("#inputTag"));
initDatePicker($('#inputPurchaseDate')); // Initialize mobile experience using shared framework
initDatePicker($('#inputSoldDate')); initializeVehicleMobile();
$('#addVehicleModal').modal('show'); $('#addVehicleModal').modal('show');
} }
}) })

View File

@@ -1,133 +1,29 @@
// Mobile detection utility // Initialize gas record modal for mobile (using shared mobile framework)
function isMobileDevice() { function initializeGasRecordMobile() {
return window.matchMedia("(max-width: 768px)").matches || initMobileModal({
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); modalId: '#gasRecordModal',
} dateInputId: '#gasRecordDate',
tagSelectorId: '#gasRecordTag',
modeToggleId: '#fuelEntryModeToggle',
simpleModeDefault: true
});
// Initialize form inputs based on device type // Handle desktop initialization
function initializeFormInputs() { if (!isMobileDevice()) {
if (isMobileDevice()) {
// Convert date input to native HTML5 on mobile
var dateInput = $('#gasRecordDate');
if (dateInput.length) {
dateInput.attr('type', 'date');
dateInput.removeClass('datepicker'); // Remove Bootstrap datepicker class
}
// Default to simple mode on mobile
var modeToggle = $('#fuelEntryModeToggle');
if (modeToggle.length && !modeToggle.is(':checked')) {
modeToggle.prop('checked', true);
toggleFuelEntryMode(true);
}
// Initialize mobile-friendly tag selector
initMobileTagSelector($("#gasRecordTag"));
} else {
// Use Bootstrap datepicker on desktop
initDatePicker($('#gasRecordDate')); initDatePicker($('#gasRecordDate'));
// Use standard tag selector on desktop
initTagSelector($("#gasRecordTag")); initTagSelector($("#gasRecordTag"));
} }
} }
// Mobile-optimized tag selector
function initMobileTagSelector(input) {
if (input.length) {
// Initialize with mobile-friendly options
input.tagsinput({
maxTags: 5, // Limit tags on mobile
trimValue: true,
confirmKeys: [13, 44, 32], // Enter, comma, space
focusClass: 'focus',
freeInput: true
});
// Add mobile-specific styling
input.parent().addClass('mobile-tag-input');
// Increase touch target size for mobile
input.parent().find('.bootstrap-tagsinput').css({
'min-height': '44px',
'padding': '8px',
'font-size': '16px' // Prevent zoom on iOS
});
// Style the input within tags
input.parent().find('.bootstrap-tagsinput input').css({
'font-size': '16px',
'min-height': '30px'
});
}
}
// Swipe to dismiss functionality for mobile modals
function initSwipeToDismiss() {
if (!isMobileDevice()) return;
var modal = $('#gasRecordModal');
var modalContent = modal.find('.modal-content');
var startY = 0;
var currentY = 0;
var isDragging = false;
var threshold = 100; // Minimum swipe distance to dismiss
// Touch start
modalContent.on('touchstart', function(e) {
startY = e.originalEvent.touches[0].clientY;
isDragging = true;
modalContent.css('transition', 'none');
});
// Touch move
modalContent.on('touchmove', function(e) {
if (!isDragging) return;
currentY = e.originalEvent.touches[0].clientY;
var deltaY = currentY - startY;
// Only allow downward swipes
if (deltaY > 0) {
modalContent.css('transform', `translateY(${deltaY}px)`);
}
});
// Touch end
modalContent.on('touchend', function(e) {
if (!isDragging) return;
isDragging = false;
var deltaY = currentY - startY;
modalContent.css('transition', 'transform 0.3s ease-out');
if (deltaY > threshold) {
// Dismiss modal
modalContent.css('transform', 'translateY(100%)');
setTimeout(function() {
hideAddGasRecordModal();
modalContent.css('transform', '');
}, 300);
} else {
// Snap back
modalContent.css('transform', '');
}
});
}
function showAddGasRecordModal() { function showAddGasRecordModal() {
$.get(`/Vehicle/GetAddGasRecordPartialView?vehicleId=${GetVehicleId().vehicleId}`, function (data) { $.get(`/Vehicle/GetAddGasRecordPartialView?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
if (data) { if (data) {
$("#gasRecordModalContent").html(data); $("#gasRecordModalContent").html(data);
// Initialize inputs based on device type // Initialize mobile experience using shared framework
initializeFormInputs(); initializeGasRecordMobile();
$('#gasRecordModal').modal('show'); $('#gasRecordModal').modal('show');
// Initialize swipe to dismiss for mobile
initSwipeToDismiss();
} }
}); });
} }
@@ -148,15 +44,12 @@ function showEditGasRecordModal(gasRecordId, nocache) {
if (data) { if (data) {
$("#gasRecordModalContent").html(data); $("#gasRecordModalContent").html(data);
// Initialize inputs based on device type // Initialize mobile experience using shared framework
initializeFormInputs(); initializeGasRecordMobile();
$('#gasRecordModal').modal('show'); $('#gasRecordModal').modal('show');
bindModalInputChanges('gasRecordModal'); bindModalInputChanges('gasRecordModal');
// Initialize swipe to dismiss for mobile
initSwipeToDismiss();
$('#gasRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { $('#gasRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) { if (getGlobalConfig().useMarkDown) {
toggleMarkDownOverlay("gasRecordNotes"); toggleMarkDownOverlay("gasRecordNotes");
@@ -569,13 +462,10 @@ function editMultipleGasRecords(ids) {
if (data) { if (data) {
$("#gasRecordModalContent").html(data); $("#gasRecordModalContent").html(data);
// Initialize inputs based on device type // Initialize mobile experience using shared framework
initializeFormInputs(); initializeGasRecordMobile();
$('#gasRecordModal').modal('show'); $('#gasRecordModal').modal('show');
// Initialize swipe to dismiss for mobile
initSwipeToDismiss();
} }
}); });
} }

View File

@@ -1,9 +1,26 @@
// Initialize reminder record modal for mobile (using shared mobile framework)
function initializeReminderRecordMobile() {
initMobileModal({
modalId: '#reminderRecordModal',
dateInputId: '#reminderDate',
tagSelectorId: '#reminderRecordTag'
});
// Handle desktop initialization
if (!isMobileDevice()) {
initDatePicker($('#reminderDate'), true);
initTagSelector($("#reminderRecordTag"));
}
}
function showEditReminderRecordModal(reminderId) { function showEditReminderRecordModal(reminderId) {
$.get(`/Vehicle/GetReminderRecordForEditById?reminderRecordId=${reminderId}`, function (data) { $.get(`/Vehicle/GetReminderRecordForEditById?reminderRecordId=${reminderId}`, function (data) {
if (data) { if (data) {
$("#reminderRecordModalContent").html(data); $("#reminderRecordModalContent").html(data);
initDatePicker($('#reminderDate'), true);
initTagSelector($("#reminderRecordTag")); // Initialize mobile experience using shared framework
initializeReminderRecordMobile();
$("#reminderRecordModal").modal("show"); $("#reminderRecordModal").modal("show");
$('#reminderRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { $('#reminderRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {
if (getGlobalConfig().useMarkDown) { if (getGlobalConfig().useMarkDown) {

View File

@@ -1,10 +1,26 @@
// Initialize service record modal for mobile (using shared mobile framework)
function initializeServiceRecordMobile() {
initMobileModal({
modalId: '#serviceRecordModal',
dateInputId: '#serviceRecordDate',
tagSelectorId: '#serviceRecordTag'
});
// Handle desktop initialization
if (!isMobileDevice()) {
initDatePicker($('#serviceRecordDate'));
initTagSelector($("#serviceRecordTag"));
}
}
function showAddServiceRecordModal() { function showAddServiceRecordModal() {
$.get('/Vehicle/GetAddServiceRecordPartialView', function (data) { $.get('/Vehicle/GetAddServiceRecordPartialView', function (data) {
if (data) { if (data) {
$("#serviceRecordModalContent").html(data); $("#serviceRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#serviceRecordDate')); // Initialize mobile experience using shared framework
initTagSelector($("#serviceRecordTag")); initializeServiceRecordMobile();
$('#serviceRecordModal').modal('show'); $('#serviceRecordModal').modal('show');
} }
}); });
@@ -25,9 +41,10 @@ function showEditServiceRecordModal(serviceRecordId, nocache) {
$.get(`/Vehicle/GetServiceRecordForEditById?serviceRecordId=${serviceRecordId}`, function (data) { $.get(`/Vehicle/GetServiceRecordForEditById?serviceRecordId=${serviceRecordId}`, function (data) {
if (data) { if (data) {
$("#serviceRecordModalContent").html(data); $("#serviceRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#serviceRecordDate')); // Initialize mobile experience using shared framework
initTagSelector($("#serviceRecordTag")); initializeServiceRecordMobile();
$('#serviceRecordModal').modal('show'); $('#serviceRecordModal').modal('show');
bindModalInputChanges('serviceRecordModal'); bindModalInputChanges('serviceRecordModal');
$('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { $('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {

View File

@@ -375,6 +375,149 @@ function initTagSelector(input, noDataList) {
input.tagsinput(); input.tagsinput();
} }
} }
// ===== MOBILE EXPERIENCE FRAMEWORK =====
// Mobile detection utility
function isMobileDevice() {
return window.matchMedia("(max-width: 768px)").matches ||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// Mobile-optimized tag selector
function initMobileTagSelector(input) {
if (input.length) {
// Initialize with mobile-friendly options
input.tagsinput({
maxTags: 5, // Limit tags on mobile
trimValue: true,
confirmKeys: [13, 44, 32], // Enter, comma, space
focusClass: 'focus',
freeInput: true
});
// Add mobile-specific styling
input.parent().addClass('mobile-tag-input');
// Increase touch target size for mobile
input.parent().find('.bootstrap-tagsinput').css({
'min-height': '44px',
'padding': '8px',
'font-size': '16px' // Prevent zoom on iOS
});
// Style the input within tags
input.parent().find('.bootstrap-tagsinput input').css({
'font-size': '16px',
'min-height': '30px'
});
}
}
// Universal mobile modal initializer
function initMobileModal(config) {
if (!isMobileDevice()) return;
var modalId = config.modalId || '#modal';
var dateInputId = config.dateInputId;
var tagSelectorId = config.tagSelectorId;
var modeToggleId = config.modeToggleId;
var simpleModeDefault = config.simpleModeDefault || false;
// Convert date input to native HTML5 on mobile
if (dateInputId) {
var dateInput = $(dateInputId);
if (dateInput.length) {
dateInput.attr('type', 'date');
dateInput.removeClass('datepicker'); // Remove Bootstrap datepicker class
}
}
// Default to simple mode on mobile if toggle exists
if (modeToggleId && simpleModeDefault) {
var modeToggle = $(modeToggleId);
if (modeToggle.length && !modeToggle.is(':checked')) {
modeToggle.prop('checked', true);
// Try to call the mode toggle function if it exists
if (typeof toggleFuelEntryMode === 'function') {
toggleFuelEntryMode(true);
}
}
}
// Initialize mobile-friendly tag selector
if (tagSelectorId) {
initMobileTagSelector($(tagSelectorId));
}
// Initialize swipe to dismiss
initSwipeToDismiss(modalId);
}
// Swipe to dismiss functionality for mobile modals
function initSwipeToDismiss(modalSelector) {
if (!isMobileDevice()) return;
modalSelector = modalSelector || '#modal';
var modal = $(modalSelector);
var modalContent = modal.find('.modal-content');
var startY = 0;
var currentY = 0;
var isDragging = false;
var threshold = 100; // Minimum swipe distance to dismiss
// Remove any existing touch handlers to avoid conflicts
modalContent.off('touchstart touchmove touchend');
// Touch start
modalContent.on('touchstart', function(e) {
startY = e.originalEvent.touches[0].clientY;
isDragging = true;
modalContent.css('transition', 'none');
});
// Touch move
modalContent.on('touchmove', function(e) {
if (!isDragging) return;
currentY = e.originalEvent.touches[0].clientY;
var deltaY = currentY - startY;
// Only allow downward swipes
if (deltaY > 0) {
modalContent.css('transform', `translateY(${deltaY}px)`);
}
});
// Touch end
modalContent.on('touchend', function(e) {
if (!isDragging) return;
isDragging = false;
var deltaY = currentY - startY;
modalContent.css('transition', 'transform 0.3s ease-out');
if (deltaY > threshold) {
// Dismiss modal
modalContent.css('transform', 'translateY(100%)');
setTimeout(function() {
modal.modal('hide');
modalContent.css('transform', '');
}, 300);
} else {
// Snap back
modalContent.css('transform', '');
}
});
}
// Initialize form inputs based on device type (legacy compatibility)
function initializeFormInputs() {
// This function is maintained for backward compatibility with gasrecord.js
// New modals should use initMobileModal() instead
console.warn('initializeFormInputs() is deprecated. Use initMobileModal() instead.');
}
function getAndValidateSelectedVehicle() { function getAndValidateSelectedVehicle() {
var selectedVehiclesArray = []; var selectedVehiclesArray = [];
$("#vehicleSelector :checked").map(function () { $("#vehicleSelector :checked").map(function () {

View File

@@ -1,10 +1,26 @@
// Initialize tax record modal for mobile (using shared mobile framework)
function initializeTaxRecordMobile() {
initMobileModal({
modalId: '#taxRecordModal',
dateInputId: '#taxRecordDate',
tagSelectorId: '#taxRecordTag'
});
// Handle desktop initialization
if (!isMobileDevice()) {
initDatePicker($('#taxRecordDate'));
initTagSelector($("#taxRecordTag"));
}
}
function showAddTaxRecordModal() { function showAddTaxRecordModal() {
$.get('/Vehicle/GetAddTaxRecordPartialView', function (data) { $.get('/Vehicle/GetAddTaxRecordPartialView', function (data) {
if (data) { if (data) {
$("#taxRecordModalContent").html(data); $("#taxRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#taxRecordDate')); // Initialize mobile experience using shared framework
initTagSelector($("#taxRecordTag")); initializeTaxRecordMobile();
$('#taxRecordModal').modal('show'); $('#taxRecordModal').modal('show');
} }
}); });
@@ -25,9 +41,10 @@ function showEditTaxRecordModal(taxRecordId, nocache) {
$.get(`/Vehicle/GetTaxRecordForEditById?taxRecordId=${taxRecordId}`, function (data) { $.get(`/Vehicle/GetTaxRecordForEditById?taxRecordId=${taxRecordId}`, function (data) {
if (data) { if (data) {
$("#taxRecordModalContent").html(data); $("#taxRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#taxRecordDate')); // Initialize mobile experience using shared framework
initTagSelector($("#taxRecordTag")); initializeTaxRecordMobile();
$('#taxRecordModal').modal('show'); $('#taxRecordModal').modal('show');
bindModalInputChanges('taxRecordModal'); bindModalInputChanges('taxRecordModal');
$('#taxRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { $('#taxRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {

View File

@@ -1,10 +1,26 @@
// Initialize upgrade record modal for mobile (using shared mobile framework)
function initializeUpgradeRecordMobile() {
initMobileModal({
modalId: '#upgradeRecordModal',
dateInputId: '#upgradeRecordDate',
tagSelectorId: '#upgradeRecordTag'
});
// Handle desktop initialization
if (!isMobileDevice()) {
initDatePicker($('#upgradeRecordDate'));
initTagSelector($("#upgradeRecordTag"));
}
}
function showAddUpgradeRecordModal() { function showAddUpgradeRecordModal() {
$.get('/Vehicle/GetAddUpgradeRecordPartialView', function (data) { $.get('/Vehicle/GetAddUpgradeRecordPartialView', function (data) {
if (data) { if (data) {
$("#upgradeRecordModalContent").html(data); $("#upgradeRecordModalContent").html(data);
//initiate datepicker
initDatePicker($('#upgradeRecordDate')); // Initialize mobile experience using shared framework
initTagSelector($("#upgradeRecordTag")); initializeUpgradeRecordMobile();
$('#upgradeRecordModal').modal('show'); $('#upgradeRecordModal').modal('show');
} }
}); });
@@ -25,9 +41,8 @@ function showEditUpgradeRecordModal(upgradeRecordId, nocache) {
$.get(`/Vehicle/GetUpgradeRecordForEditById?upgradeRecordId=${upgradeRecordId}`, function (data) { $.get(`/Vehicle/GetUpgradeRecordForEditById?upgradeRecordId=${upgradeRecordId}`, function (data) {
if (data) { if (data) {
$("#upgradeRecordModalContent").html(data); $("#upgradeRecordModalContent").html(data);
//initiate datepicker // Initialize mobile experience using shared framework
initDatePicker($('#upgradeRecordDate')); initializeUpgradeRecordMobile();
initTagSelector($("#upgradeRecordTag"));
$('#upgradeRecordModal').modal('show'); $('#upgradeRecordModal').modal('show');
bindModalInputChanges('upgradeRecordModal'); bindModalInputChanges('upgradeRecordModal');
$('#upgradeRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { $('#upgradeRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () {