feat: replace all native date inputs with custom DatePicker component
Add theme-aware DatePicker with pill display, calendar dropdown, prev/next day arrows, bilingual month/weekday names, and min/max support. Replace all 15 native <input type="date"> elements across fitness, tasks, and cospend.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.23.4",
|
"version": "1.23.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
<script>
|
||||||
|
import { ChevronLeft, ChevronRight, Calendar } from '@lucide/svelte';
|
||||||
|
|
||||||
|
let { value = $bindable(''), lang = 'en', min = '', max = '' } = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let pickerRef = $state(null);
|
||||||
|
|
||||||
|
const WEEKDAYS_EN = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
||||||
|
const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||||
|
const MONTHS_EN = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
const MONTHS_DE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||||
|
|
||||||
|
const weekdays = $derived(lang === 'de' ? WEEKDAYS_DE : WEEKDAYS_EN);
|
||||||
|
const months = $derived(lang === 'de' ? MONTHS_DE : MONTHS_EN);
|
||||||
|
|
||||||
|
// The month being viewed in the calendar (independent of selected value)
|
||||||
|
let viewYear = $state(0);
|
||||||
|
let viewMonth = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (value) {
|
||||||
|
const d = new Date(value + 'T12:00:00');
|
||||||
|
viewYear = d.getFullYear();
|
||||||
|
viewMonth = d.getMonth();
|
||||||
|
} else {
|
||||||
|
const now = new Date();
|
||||||
|
viewYear = now.getFullYear();
|
||||||
|
viewMonth = now.getMonth();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const todayStr = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const displayDate = $derived.by(() => {
|
||||||
|
if (!value) return lang === 'en' ? 'Select date' : 'Datum wählen';
|
||||||
|
if (value === todayStr) return lang === 'en' ? 'Today' : 'Heute';
|
||||||
|
const d = new Date(value + 'T12:00:00');
|
||||||
|
return d.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||||
|
});
|
||||||
|
|
||||||
|
function isDisabled(dateStr) {
|
||||||
|
if (min && dateStr < min) return true;
|
||||||
|
if (max && dateStr > max) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateDate(delta) {
|
||||||
|
const d = new Date((value || todayStr) + 'T12:00:00');
|
||||||
|
d.setDate(d.getDate() + delta);
|
||||||
|
const next = d.toISOString().slice(0, 10);
|
||||||
|
if (!isDisabled(next)) value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navMonth(delta) {
|
||||||
|
viewMonth += delta;
|
||||||
|
if (viewMonth > 11) { viewMonth = 0; viewYear++; }
|
||||||
|
if (viewMonth < 0) { viewMonth = 11; viewYear--; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDay(dateStr) {
|
||||||
|
value = dateStr;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToday() {
|
||||||
|
value = todayStr;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarDays = $derived.by(() => {
|
||||||
|
const first = new Date(viewYear, viewMonth, 1);
|
||||||
|
// Monday=0 based offset
|
||||||
|
let startDay = first.getDay() - 1;
|
||||||
|
if (startDay < 0) startDay = 6;
|
||||||
|
|
||||||
|
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
||||||
|
const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate();
|
||||||
|
|
||||||
|
/** @type {{ date: string, day: number, currentMonth: boolean, isToday: boolean, isSelected: boolean }[]} */
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
// Previous month trailing days
|
||||||
|
for (let i = startDay - 1; i >= 0; i--) {
|
||||||
|
const d = daysInPrevMonth - i;
|
||||||
|
const m = viewMonth === 0 ? 11 : viewMonth - 1;
|
||||||
|
const y = viewMonth === 0 ? viewYear - 1 : viewYear;
|
||||||
|
const dateStr = `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||||
|
days.push({ date: dateStr, day: d, currentMonth: false, isToday: dateStr === todayStr, isSelected: dateStr === value, disabled: isDisabled(dateStr) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current month
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const dateStr = `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||||
|
days.push({ date: dateStr, day: d, currentMonth: true, isToday: dateStr === todayStr, isSelected: dateStr === value, disabled: isDisabled(dateStr) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next month leading days (fill to complete rows of 7)
|
||||||
|
const remaining = 7 - (days.length % 7);
|
||||||
|
if (remaining < 7) {
|
||||||
|
for (let d = 1; d <= remaining; d++) {
|
||||||
|
const m = viewMonth === 11 ? 0 : viewMonth + 1;
|
||||||
|
const y = viewMonth === 11 ? viewYear + 1 : viewYear;
|
||||||
|
const dateStr = `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||||
|
days.push({ date: dateStr, day: d, currentMonth: false, isToday: dateStr === todayStr, isSelected: dateStr === value, disabled: isDisabled(dateStr) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
function handleClickOutside(e) {
|
||||||
|
if (pickerRef && !pickerRef.contains(e.target)) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener('pointerdown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('pointerdown', handleClickOutside);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="datepicker" bind:this={pickerRef}>
|
||||||
|
<div class="dp-pill">
|
||||||
|
<button type="button" class="dp-arrow" onclick={() => navigateDate(-1)} aria-label="Previous day">
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dp-display" onclick={() => open = !open}>
|
||||||
|
<Calendar size={14} />
|
||||||
|
{displayDate}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dp-arrow" onclick={() => navigateDate(1)} aria-label="Next day">
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div class="dp-dropdown">
|
||||||
|
<div class="dp-header">
|
||||||
|
<button type="button" class="dp-nav" onclick={() => navMonth(-1)} aria-label="Previous month">
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<span class="dp-month-label">{months[viewMonth]} {viewYear}</span>
|
||||||
|
<button type="button" class="dp-nav" onclick={() => navMonth(1)} aria-label="Next month">
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dp-weekdays">
|
||||||
|
{#each weekdays as wd (wd)}
|
||||||
|
<span class="dp-wd">{wd}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dp-grid">
|
||||||
|
{#each calendarDays as day (day.date)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dp-day"
|
||||||
|
class:other-month={!day.currentMonth}
|
||||||
|
class:today={day.isToday}
|
||||||
|
class:selected={day.isSelected}
|
||||||
|
class:disabled={day.disabled}
|
||||||
|
disabled={day.disabled}
|
||||||
|
onclick={() => selectDay(day.date)}
|
||||||
|
>
|
||||||
|
{day.day}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if value !== todayStr}
|
||||||
|
<button type="button" class="dp-today-btn" onclick={goToday}>
|
||||||
|
{lang === 'en' ? 'Today' : 'Heute'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.datepicker {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pill row */
|
||||||
|
.dp-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.dp-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.35rem 0.4rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--transition-normal), background var(--transition-normal);
|
||||||
|
}
|
||||||
|
.dp-arrow:hover {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
}
|
||||||
|
.dp-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color var(--transition-normal), background var(--transition-normal);
|
||||||
|
}
|
||||||
|
.dp-display:hover {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown calendar */
|
||||||
|
.dp-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.4rem);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
padding: 0.6rem;
|
||||||
|
z-index: 200;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
.dp-month-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
.dp-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-normal), color var(--transition-normal);
|
||||||
|
}
|
||||||
|
.dp-nav:hover {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp-weekdays {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.dp-wd {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.dp-day {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-normal), color var(--transition-normal);
|
||||||
|
}
|
||||||
|
.dp-day:hover {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
}
|
||||||
|
.dp-day.other-month {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
.dp-day.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.dp-day.today {
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: inset 0 0 0 1.5px var(--color-primary);
|
||||||
|
}
|
||||||
|
.dp-day.selected {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-text-on-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.dp-day.selected:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp-today-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
padding: 0.3rem;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-normal);
|
||||||
|
}
|
||||||
|
.dp-today-btn:hover {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { t } from '$lib/js/fitnessI18n';
|
import { t } from '$lib/js/fitnessI18n';
|
||||||
import { Trash2, Plus, Pencil, UserPlus, X, ChevronLeft, ChevronRight } from '@lucide/svelte';
|
import { Trash2, Plus, Pencil, UserPlus, X, ChevronLeft, ChevronRight } from '@lucide/svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
import { toast } from '$lib/js/toast.svelte';
|
import { toast } from '$lib/js/toast.svelte';
|
||||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||||
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
||||||
@@ -748,11 +749,11 @@
|
|||||||
<div class="add-row">
|
<div class="add-row">
|
||||||
<label>
|
<label>
|
||||||
{t('period_start', lang)}
|
{t('period_start', lang)}
|
||||||
<input type="date" bind:value={addStart} max={todayStr} />
|
<DatePicker bind:value={addStart} max={todayStr} {lang} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t('period_end', lang)}
|
{t('period_end', lang)}
|
||||||
<input type="date" bind:value={addEnd} min={addStart} max={todayStr} />
|
<DatePicker bind:value={addEnd} min={addStart} max={todayStr} {lang} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-actions">
|
<div class="add-actions">
|
||||||
@@ -773,11 +774,11 @@
|
|||||||
<div class="add-row">
|
<div class="add-row">
|
||||||
<label>
|
<label>
|
||||||
{t('period_start', lang)}
|
{t('period_start', lang)}
|
||||||
<input type="date" bind:value={editStart} />
|
<DatePicker bind:value={editStart} {lang} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t('period_end', lang)}
|
{t('period_end', lang)}
|
||||||
<input type="date" bind:value={editEnd} min={editStart} />
|
<DatePicker bind:value={editEnd} min={editStart} {lang} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-actions">
|
<div class="add-actions">
|
||||||
@@ -836,11 +837,11 @@
|
|||||||
<div class="add-row">
|
<div class="add-row">
|
||||||
<label>
|
<label>
|
||||||
{t('period_start', lang)}
|
{t('period_start', lang)}
|
||||||
<input type="date" bind:value={addStart} max={todayStr} />
|
<DatePicker bind:value={addStart} max={todayStr} {lang} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t('period_end', lang)}
|
{t('period_end', lang)}
|
||||||
<input type="date" bind:value={addEnd} min={addStart} max={todayStr} />
|
<DatePicker bind:value={addEnd} min={addStart} max={todayStr} {lang} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-actions">
|
<div class="add-actions">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Flower2, Droplets, Leaf, ShoppingCart, Trash2, Shirt, Brush } from '@lucide/svelte';
|
Flower2, Droplets, Leaf, ShoppingCart, Trash2, Shirt, Brush } from '@lucide/svelte';
|
||||||
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
||||||
import Toggle from '$lib/components/Toggle.svelte';
|
import Toggle from '$lib/components/Toggle.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
|
||||||
const USERS = ['anna', 'alexander'];
|
const USERS = ['anna', 'alexander'];
|
||||||
|
|
||||||
@@ -306,7 +307,7 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="dueDate">Fällig am</label>
|
<label for="dueDate">Fällig am</label>
|
||||||
<input id="dueDate" type="date" bind:value={nextDueDate} required />
|
<DatePicker bind:value={nextDueDate} lang="de" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import ImageUpload from '$lib/components/ImageUpload.svelte';
|
import ImageUpload from '$lib/components/ImageUpload.svelte';
|
||||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||||
import Toggle from '$lib/components/Toggle.svelte';
|
import Toggle from '$lib/components/Toggle.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -454,13 +455,8 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="date">{t('payment_date', lang)}</label>
|
<label for="date">{t('payment_date', lang)}</label>
|
||||||
<input
|
<DatePicker bind:value={formData.date} {lang} />
|
||||||
type="date"
|
<input type="hidden" name="date" value={formData.date} />
|
||||||
id="date"
|
|
||||||
name="date"
|
|
||||||
bind:value={formData.date}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{#if formData.currency !== 'CHF'}
|
{#if formData.currency !== 'CHF'}
|
||||||
<small class="help-text">{t('exchange_rate_date', lang)}</small>
|
<small class="help-text">{t('exchange_rate_date', lang)}</small>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -505,13 +501,8 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="recurringStartDate">{t('start_date', lang)}</label>
|
<label for="recurringStartDate">{t('start_date', lang)}</label>
|
||||||
<input
|
<DatePicker bind:value={recurringData.startDate} {lang} />
|
||||||
type="date"
|
<input type="hidden" name="recurringStartDate" value={recurringData.startDate} />
|
||||||
id="recurringStartDate"
|
|
||||||
name="recurringStartDate"
|
|
||||||
bind:value={recurringData.startDate}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -545,13 +536,8 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="recurringEndDate">{t('end_date_optional', lang)}</label>
|
<label for="recurringEndDate">{t('end_date_optional', lang)}</label>
|
||||||
<input
|
<DatePicker bind:value={recurringData.endDate} min={recurringData.startDate} {lang} />
|
||||||
type="date"
|
<input type="hidden" name="recurringEndDate" value={recurringData.endDate} />
|
||||||
id="recurringEndDate"
|
|
||||||
name="recurringEndDate"
|
|
||||||
bind:value={recurringData.endDate}
|
|
||||||
min={recurringData.startDate}
|
|
||||||
/>
|
|
||||||
<small class="help-text">{t('end_date_hint', lang)}</small>
|
<small class="help-text">{t('end_date_hint', lang)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import FormSection from '$lib/components/FormSection.svelte';
|
import FormSection from '$lib/components/FormSection.svelte';
|
||||||
import ImageUpload from '$lib/components/ImageUpload.svelte';
|
import ImageUpload from '$lib/components/ImageUpload.svelte';
|
||||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('$models/Payment').IPayment & {splits?: import('$models/PaymentSplit').IPaymentSplit[]}} PaymentWithSplits
|
* @typedef {import('$models/Payment').IPayment & {splits?: import('$models/PaymentSplit').IPaymentSplit[]}} PaymentWithSplits
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
let jsEnhanced = $state(false);
|
let jsEnhanced = $state(false);
|
||||||
/** @type {number | null} */
|
/** @type {number | null} */
|
||||||
let originalAmount = $state(null);
|
let originalAmount = $state(null);
|
||||||
|
let paymentDateStr = $state('');
|
||||||
|
|
||||||
let categoryOptions = $derived(getCategoryOptionsI18n(lang));
|
let categoryOptions = $derived(getCategoryOptionsI18n(lang));
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paymentDateStr = formatDateForInput(loaded.date);
|
||||||
// Store original amount for comparison to prevent infinite recalculation
|
// Store original amount for comparison to prevent infinite recalculation
|
||||||
originalAmount = loaded.amount;
|
originalAmount = loaded.amount;
|
||||||
// Set initial lastCalculatedAmount to prevent immediate recalculation on load
|
// Set initial lastCalculatedAmount to prevent immediate recalculation on load
|
||||||
@@ -343,6 +346,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync date picker string back to payment object
|
||||||
|
$effect(() => {
|
||||||
|
if (payment && paymentDateStr) {
|
||||||
|
payment.date = /** @type {Date} */ (new Date(paymentDateStr + 'T12:00:00'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Reactive statement for exchange rate fetching
|
// Reactive statement for exchange rate fetching
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (jsEnhanced && payment && payment.currency && payment.currency !== 'CHF' && payment.date && payment.originalAmount) {
|
if (jsEnhanced && payment && payment.currency && payment.currency !== 'CHF' && payment.date && payment.originalAmount) {
|
||||||
@@ -469,13 +479,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="date">{t('date', lang)}</label>
|
<label for="date">{t('date', lang)}</label>
|
||||||
<input
|
<DatePicker bind:value={paymentDateStr} {lang} />
|
||||||
type="date"
|
|
||||||
id="date"
|
|
||||||
value={formatDateForInput(payment.date)}
|
|
||||||
onchange={(e) => { if (payment) payment.date = /** @type {Date} */ (new Date(/** @type {HTMLInputElement} */ (e.target).value)); }}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import SplitMethodSelector from '$lib/components/cospend/SplitMethodSelector.svelte';
|
import SplitMethodSelector from '$lib/components/cospend/SplitMethodSelector.svelte';
|
||||||
import UsersList from '$lib/components/cospend/UsersList.svelte';
|
import UsersList from '$lib/components/cospend/UsersList.svelte';
|
||||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -416,12 +417,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="startDate">{t('start_date', lang)}</label>
|
<label for="startDate">{t('start_date', lang)}</label>
|
||||||
<input
|
<DatePicker bind:value={formData.startDate} {lang} />
|
||||||
type="date"
|
|
||||||
id="startDate"
|
|
||||||
bind:value={formData.startDate}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -454,11 +450,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="endDate">{t('end_date_optional', lang)}</label>
|
<label for="endDate">{t('end_date_optional', lang)}</label>
|
||||||
<input
|
<DatePicker bind:value={formData.endDate} {lang} />
|
||||||
type="date"
|
|
||||||
id="endDate"
|
|
||||||
bind:value={formData.endDate}
|
|
||||||
/>
|
|
||||||
<div class="help-text">{t('end_date_hint', lang)}</div>
|
<div class="help-text">{t('end_date_hint', lang)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import ExerciseName from '$lib/components/fitness/ExerciseName.svelte';
|
import ExerciseName from '$lib/components/fitness/ExerciseName.svelte';
|
||||||
import SetTable from '$lib/components/fitness/SetTable.svelte';
|
import SetTable from '$lib/components/fitness/SetTable.svelte';
|
||||||
import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte';
|
import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
@@ -503,7 +504,7 @@
|
|||||||
<div class="edit-meta">
|
<div class="edit-meta">
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<label for="edit-date">{t('date', lang)}</label>
|
<label for="edit-date">{t('date', lang)}</label>
|
||||||
<input id="edit-date" type="date" bind:value={editData.date} />
|
<DatePicker bind:value={editData.date} {lang} />
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<label for="edit-time">{t('time', lang)}</label>
|
<label for="edit-time">{t('time', lang)}</label>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { toast } from '$lib/js/toast.svelte';
|
import { toast } from '$lib/js/toast.svelte';
|
||||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
|
||||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||||
@@ -289,7 +290,7 @@
|
|||||||
<!-- New measurement form -->
|
<!-- New measurement form -->
|
||||||
<form class="add-form" onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
|
<form class="add-form" onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
|
||||||
<div class="date-row">
|
<div class="date-row">
|
||||||
<input type="date" bind:value={formDate} class="date-pill" />
|
<DatePicker bind:value={formDate} {lang} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="weight-card">
|
<div class="weight-card">
|
||||||
@@ -588,20 +589,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.date-pill {
|
|
||||||
background: var(--color-bg-tertiary);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
padding: 0.35rem 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.date-pill:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.weight-card {
|
.weight-card {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||||
import { Trash2 } from '@lucide/svelte';
|
import { Trash2 } from '@lucide/svelte';
|
||||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
|
||||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
<form onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
|
<form onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="m-date">{t('date', lang)}</label>
|
<label for="m-date">{t('date', lang)}</label>
|
||||||
<input id="m-date" type="date" bind:value={formDate} />
|
<DatePicker bind:value={formDate} {lang} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>{t('general', lang)}</h3>
|
<h3>{t('general', lang)}</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user