refactored view package
This commit is contained in:
154
internal/view/datepicker/README.md
Normal file
154
internal/view/datepicker/README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# DatePicker Component
|
||||
|
||||
A reusable, standalone date picker component for oslstats forms.
|
||||
|
||||
## Features
|
||||
|
||||
✨ **Interactive Calendar**
|
||||
- Click-to-open dropdown calendar
|
||||
- Month and year dropdown selectors for easy navigation
|
||||
- Previous/next month arrow buttons
|
||||
- Today's date highlighted in blue
|
||||
|
||||
📅 **Date Format**
|
||||
- DD/MM/YYYY format (the sensible way)
|
||||
- Pattern validation built-in
|
||||
- Read-only input (date selection via picker only)
|
||||
|
||||
🎨 **Design**
|
||||
- Consistent with Catppuccin theme
|
||||
- Responsive layout
|
||||
- Hover effects and visual feedback
|
||||
- Calendar icon in input field
|
||||
|
||||
🔧 **Technical**
|
||||
- Built with Alpine.js (already in project)
|
||||
- No external dependencies
|
||||
- Year range: 2001-2051
|
||||
- Submits in DD/MM/YYYY format
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```templ
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/component/datepicker"
|
||||
|
||||
// Simple usage - required field, no custom onChange
|
||||
@datepicker.DatePicker(
|
||||
"event_date", // id
|
||||
"event_date", // name
|
||||
"Event Date", // label
|
||||
"DD/MM/YYYY", // placeholder
|
||||
true, // required
|
||||
"" // onChange (empty = none)
|
||||
)
|
||||
```
|
||||
|
||||
### With Custom onChange Handler
|
||||
|
||||
```templ
|
||||
// With Alpine.js validation logic
|
||||
@datepicker.DatePicker(
|
||||
"start_date",
|
||||
"start_date",
|
||||
"Start Date",
|
||||
"DD/MM/YYYY",
|
||||
true,
|
||||
"dateIsEmpty = false; validateForm();"
|
||||
)
|
||||
```
|
||||
|
||||
### Optional Field
|
||||
|
||||
```templ
|
||||
// Non-required field
|
||||
@datepicker.DatePicker(
|
||||
"end_date",
|
||||
"end_date",
|
||||
"End Date (Optional)",
|
||||
"DD/MM/YYYY",
|
||||
false, // not required
|
||||
""
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description | Example |
|
||||
|-------------|--------|------------------------------------------------------|----------------------------------|
|
||||
| `id` | string | Unique ID for the input element | `"start_date"` |
|
||||
| `name` | string | Form field name (used in form submission) | `"start_date"` |
|
||||
| `label` | string | Display label above the input | `"Start Date"` |
|
||||
| `placeholder` | string | Placeholder text in the input | `"DD/MM/YYYY"` |
|
||||
| `required` | bool | Whether the field is required | `true` or `false` |
|
||||
| `onChange` | string | Alpine.js expression to run when date changes | `"updateForm();"` or `""` |
|
||||
|
||||
## Server-Side Parsing
|
||||
|
||||
The date picker submits dates in **DD/MM/YYYY** format. Parse on the server like this:
|
||||
|
||||
```go
|
||||
import "time"
|
||||
|
||||
// Parse DD/MM/YYYY format
|
||||
dateStr := r.FormValue("start_date") // e.g., "15/02/2026"
|
||||
date, err := time.Parse("02/01/2006", dateStr)
|
||||
if err != nil {
|
||||
// Handle invalid date
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: The format string is `"02/01/2006"` (DD/MM/YYYY), NOT `"01/02/2006"` (MM/DD/YYYY).
|
||||
|
||||
## Integration with Alpine.js Forms
|
||||
|
||||
The date picker works seamlessly with Alpine.js validation:
|
||||
|
||||
```templ
|
||||
<form x-data="{
|
||||
dateIsEmpty: true,
|
||||
dateError: '',
|
||||
canSubmit: false,
|
||||
updateCanSubmit() {
|
||||
this.canSubmit = !this.dateIsEmpty;
|
||||
}
|
||||
}">
|
||||
@datepicker.DatePicker(
|
||||
"event_date",
|
||||
"event_date",
|
||||
"Event Date",
|
||||
"DD/MM/YYYY",
|
||||
true,
|
||||
"dateIsEmpty = $el.value === ''; updateCanSubmit();"
|
||||
)
|
||||
|
||||
<!-- Error message -->
|
||||
<p x-show="dateError" x-text="dateError"></p>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button x-bind:disabled="!canSubmit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The component uses Tailwind CSS classes and Catppuccin theme colors:
|
||||
- `bg-mantle`, `bg-surface0`, `bg-surface1` - backgrounds
|
||||
- `border-surface1` - borders
|
||||
- `text-subtext0` - muted text
|
||||
- `bg-blue` - accent color (today's date)
|
||||
- `focus:border-blue` - focus states
|
||||
|
||||
All styling is built-in; no additional CSS required.
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
Works in all modern browsers that support:
|
||||
- Alpine.js 3.x
|
||||
- ES6 JavaScript (arrow functions, template literals, etc.)
|
||||
- CSS Grid
|
||||
|
||||
## Examples in Codebase
|
||||
|
||||
See `internal/view/component/form/new_season.templ` for a real-world usage example.
|
||||
239
internal/view/datepicker/datepicker.templ
Normal file
239
internal/view/datepicker/datepicker.templ
Normal file
@@ -0,0 +1,239 @@
|
||||
package datepicker
|
||||
|
||||
// DatePicker renders a reusable date picker component with DD/MM/YYYY format
|
||||
//
|
||||
// Features:
|
||||
// - Interactive calendar dropdown with month/year selectors
|
||||
// - DD/MM/YYYY format (proper date format, none of that American nonsense)
|
||||
// - Year range: 2001-2051
|
||||
// - Today's date highlighted in blue
|
||||
// - Click outside to close
|
||||
// - Consistent with Catppuccin theme
|
||||
//
|
||||
// Parameters:
|
||||
// - id: unique ID for the input field (e.g., "start_date")
|
||||
// - name: form field name (e.g., "start_date")
|
||||
// - label: display label (e.g., "Start Date")
|
||||
// - placeholder: input placeholder (default: "DD/MM/YYYY")
|
||||
// - required: whether the field is required (true/false)
|
||||
// - onChange: Alpine.js expression to run when date changes (e.g., "dateIsEmpty = false; updateCanSubmit();")
|
||||
// Set to empty string "" if no custom onChange handler is needed
|
||||
//
|
||||
// Usage Example:
|
||||
// import "git.haelnorr.com/h/oslstats/internal/view/component/datepicker"
|
||||
//
|
||||
// @datepicker.DatePicker("birth_date", "birth_date", "Date of Birth", "DD/MM/YYYY", true, "handleDateChange();")
|
||||
//
|
||||
// The component submits the date in DD/MM/YYYY format. To parse on the server:
|
||||
// date, err := time.Parse("02/01/2006", dateString)
|
||||
templ DatePicker(id, name, label, placeholder string, required bool, onChange string) {
|
||||
<div
|
||||
x-data="{
|
||||
showPicker: false,
|
||||
selectedDate: '',
|
||||
displayDate: '',
|
||||
tempYear: new Date().getFullYear(),
|
||||
tempMonth: new Date().getMonth(),
|
||||
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
init() {
|
||||
// Ensure dropdowns are initialized with current month/year
|
||||
this.tempYear = new Date().getFullYear();
|
||||
this.tempMonth = new Date().getMonth();
|
||||
},
|
||||
getDaysInMonth(year, month) {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
},
|
||||
getFirstDayOfMonth(year, month) {
|
||||
return new Date(year, month, 1).getDay();
|
||||
},
|
||||
selectDate(day) {
|
||||
const d = String(day).padStart(2, '0');
|
||||
const m = String(this.tempMonth + 1).padStart(2, '0');
|
||||
const y = this.tempYear;
|
||||
this.displayDate = d + '/' + m + '/' + y;
|
||||
this.selectedDate = this.displayDate;
|
||||
$refs.dateInput.value = this.displayDate;
|
||||
this.showPicker = false;
|
||||
// Manually trigger input event so Alpine.js @input handler fires
|
||||
$refs.dateInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
},
|
||||
prevMonth() {
|
||||
if (this.tempMonth === 0) {
|
||||
this.tempMonth = 11;
|
||||
this.tempYear--;
|
||||
} else {
|
||||
this.tempMonth--;
|
||||
}
|
||||
},
|
||||
nextMonth() {
|
||||
if (this.tempMonth === 11) {
|
||||
this.tempMonth = 0;
|
||||
this.tempYear++;
|
||||
} else {
|
||||
this.tempMonth++;
|
||||
}
|
||||
},
|
||||
isToday(day) {
|
||||
const today = new Date();
|
||||
return day === today.getDate() &&
|
||||
this.tempMonth === today.getMonth() &&
|
||||
this.tempYear === today.getFullYear();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<label for={ id } class="block text-sm font-medium mb-2">{ label }</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id={ id }
|
||||
name={ name }
|
||||
x-ref="dateInput"
|
||||
class="py-3 px-4 pr-10 block w-full rounded-lg text-sm bg-base border-2 border-overlay0 focus:border-blue outline-none"
|
||||
placeholder={ placeholder }
|
||||
pattern="\d{2}/\d{2}/\d{4}"
|
||||
if required {
|
||||
required
|
||||
}
|
||||
readonly
|
||||
x-model="displayDate"
|
||||
@click="showPicker = !showPicker"
|
||||
if onChange != "" {
|
||||
@input={ onChange }
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-subtext0 hover:text-text"
|
||||
@click="showPicker = !showPicker"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Date Picker Dropdown -->
|
||||
<div
|
||||
x-show="showPicker"
|
||||
x-cloak
|
||||
@click.away="showPicker = false"
|
||||
class="absolute z-50 mt-2 bg-mantle border-2 border-surface1 rounded-lg shadow-lg p-4 w-80"
|
||||
>
|
||||
<!-- Month/Year Navigation -->
|
||||
<div class="flex items-center justify-between gap-2 mb-4">
|
||||
<button type="button" @click="prevMonth()" class="p-1 hover:bg-surface0 rounded shrink-0">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex gap-2 flex-1">
|
||||
<select
|
||||
x-model.number="tempMonth"
|
||||
class="flex-1 px-2 py-1 text-sm bg-surface0 border border-surface1 rounded outline-none focus:border-blue"
|
||||
>
|
||||
<option value="0">January</option>
|
||||
<option value="1">February</option>
|
||||
<option value="2">March</option>
|
||||
<option value="3">April</option>
|
||||
<option value="4">May</option>
|
||||
<option value="5">June</option>
|
||||
<option value="6">July</option>
|
||||
<option value="7">August</option>
|
||||
<option value="8">September</option>
|
||||
<option value="9">October</option>
|
||||
<option value="10">November</option>
|
||||
<option value="11">December</option>
|
||||
</select>
|
||||
<select
|
||||
x-model.number="tempYear"
|
||||
class="px-2 py-1 text-sm bg-surface0 border border-surface1 rounded outline-none focus:border-blue"
|
||||
>
|
||||
<option value="2001">2001</option>
|
||||
<option value="2002">2002</option>
|
||||
<option value="2003">2003</option>
|
||||
<option value="2004">2004</option>
|
||||
<option value="2005">2005</option>
|
||||
<option value="2006">2006</option>
|
||||
<option value="2007">2007</option>
|
||||
<option value="2008">2008</option>
|
||||
<option value="2009">2009</option>
|
||||
<option value="2010">2010</option>
|
||||
<option value="2011">2011</option>
|
||||
<option value="2012">2012</option>
|
||||
<option value="2013">2013</option>
|
||||
<option value="2014">2014</option>
|
||||
<option value="2015">2015</option>
|
||||
<option value="2016">2016</option>
|
||||
<option value="2017">2017</option>
|
||||
<option value="2018">2018</option>
|
||||
<option value="2019">2019</option>
|
||||
<option value="2020">2020</option>
|
||||
<option value="2021">2021</option>
|
||||
<option value="2022">2022</option>
|
||||
<option value="2023">2023</option>
|
||||
<option value="2024">2024</option>
|
||||
<option value="2025">2025</option>
|
||||
<option value="2026">2026</option>
|
||||
<option value="2027">2027</option>
|
||||
<option value="2028">2028</option>
|
||||
<option value="2029">2029</option>
|
||||
<option value="2030">2030</option>
|
||||
<option value="2031">2031</option>
|
||||
<option value="2032">2032</option>
|
||||
<option value="2033">2033</option>
|
||||
<option value="2034">2034</option>
|
||||
<option value="2035">2035</option>
|
||||
<option value="2036">2036</option>
|
||||
<option value="2037">2037</option>
|
||||
<option value="2038">2038</option>
|
||||
<option value="2039">2039</option>
|
||||
<option value="2040">2040</option>
|
||||
<option value="2041">2041</option>
|
||||
<option value="2042">2042</option>
|
||||
<option value="2043">2043</option>
|
||||
<option value="2044">2044</option>
|
||||
<option value="2045">2045</option>
|
||||
<option value="2046">2046</option>
|
||||
<option value="2047">2047</option>
|
||||
<option value="2048">2048</option>
|
||||
<option value="2049">2049</option>
|
||||
<option value="2050">2050</option>
|
||||
<option value="2051">2051</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" @click="nextMonth()" class="p-1 hover:bg-surface0 rounded shrink-0">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Day Headers -->
|
||||
<div class="grid grid-cols-7 gap-1 mb-2">
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">Su</div>
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">Mo</div>
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">Tu</div>
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">We</div>
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">Th</div>
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">Fr</div>
|
||||
<div class="text-center text-xs font-medium text-subtext0 py-1">Sa</div>
|
||||
</div>
|
||||
<!-- Calendar Days -->
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<template x-for="blank in getFirstDayOfMonth(tempYear, tempMonth)" :key="'blank-' + blank">
|
||||
<div class="text-center py-2"></div>
|
||||
</template>
|
||||
<template x-for="day in getDaysInMonth(tempYear, tempMonth)" :key="'day-' + day">
|
||||
<button
|
||||
type="button"
|
||||
@click="selectDate(day)"
|
||||
class="text-center py-2 rounded hover:bg-surface0 transition"
|
||||
:class="{
|
||||
'bg-blue text-mantle font-bold': isToday(day),
|
||||
'hover:bg-surface1': !isToday(day)
|
||||
}"
|
||||
x-text="day"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user