1
0
mirror of https://github.com/axzilla/templui.git synced 2025-02-21 00:32:58 +00:00

feat: add datepicker component

This commit is contained in:
“axzilla” 2024-10-07 10:12:07 +02:00
parent aea8a59b85
commit 48d807ce6a
7 changed files with 422 additions and 0 deletions

View File

@ -4,6 +4,7 @@
- Added: Input component
- Added: Accordion component
- Added: Datepicker component
- Changed: Add button type prop
- Changed: Use own input component on tabs example
- Changed: Update all current components with library comments

View File

@ -759,6 +759,14 @@ body {
margin-top: 1rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mt-12 {
margin-top: 3rem;
}
.block {
display: block;
}
@ -779,6 +787,10 @@ body {
display: none;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
.h-1\/2 {
height: 50%;
}
@ -819,6 +831,10 @@ body {
height: 100%;
}
.h-7 {
height: 1.75rem;
}
.\!max-h-\[501px\] {
max-height: 501px !important;
}
@ -863,6 +879,14 @@ body {
width: 100%;
}
.w-7 {
width: 1.75rem;
}
.w-\[17rem\] {
width: 17rem;
}
.max-w-3xl {
max-width: 48rem;
}
@ -883,6 +907,10 @@ body {
max-width: 20rem;
}
.max-w-lg {
max-width: 32rem;
}
.flex-1 {
flex: 1 1 0%;
}
@ -953,6 +981,10 @@ body {
list-style-type: disc;
}
.grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
@ -985,6 +1017,10 @@ body {
gap: 1rem;
}
.gap-1 {
gap: 0.25rem;
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@ -1074,6 +1110,10 @@ body {
border-radius: calc(var(--radius) - 2px);
}
.rounded-full {
border-radius: 9999px;
}
.border {
border-width: 1px;
}
@ -1104,6 +1144,14 @@ body {
border-color: hsl(var(--input) / var(--tw-border-opacity));
}
.border-neutral-200\/70 {
border-color: rgb(229 229 229 / 0.7);
}
.border-transparent {
border-color: transparent;
}
.bg-background {
--tw-bg-opacity: 1;
background-color: hsl(var(--background) / var(--tw-bg-opacity));
@ -1148,6 +1196,35 @@ body {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.bg-neutral-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 229 229 / var(--tw-bg-opacity));
}
.bg-neutral-800 {
--tw-bg-opacity: 1;
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
}
.bg-opacity-75 {
--tw-bg-opacity: 0.75;
}
.bg-\[url\(\'\/assets\/img\/grid\.svg\'\)\] {
background-image: url('/assets/img/grid.svg');
}
@ -1260,6 +1337,11 @@ body {
padding-bottom: 2rem;
}
.px-0\.5 {
padding-left: 0.125rem;
padding-right: 0.125rem;
}
.pb-4 {
padding-bottom: 1rem;
}
@ -1340,6 +1422,10 @@ body {
font-weight: 600;
}
.font-normal {
font-weight: 400;
}
.uppercase {
text-transform: uppercase;
}
@ -1417,6 +1503,16 @@ body {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.text-neutral-400 {
--tw-text-opacity: 1;
color: rgb(163 163 163 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
@ -1425,6 +1521,11 @@ body {
text-underline-offset: 4px;
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.opacity-0 {
opacity: 0;
}
@ -1445,6 +1546,12 @@ body {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline {
outline-style: solid;
}
@ -1493,6 +1600,10 @@ body {
transition-duration: 300ms;
}
.duration-100 {
transition-duration: 100ms;
}
.ease-in {
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
@ -1501,6 +1612,10 @@ body {
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.\[mask-image\:linear-gradient\(180deg\2c white\2c rgba\(255\2c 255\2c 255\2c 0\)\)\] {
-webkit-mask-image: linear-gradient(180deg,white,rgba(255,255,255,0));
mask-image: linear-gradient(180deg,white,rgba(255,255,255,0));
@ -1565,6 +1680,20 @@ body {
background-color: hsl(var(--secondary) / 0.8);
}
.hover\:bg-blue-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
}
.hover\:bg-neutral-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 229 229 / var(--tw-bg-opacity));
}
.hover\:bg-opacity-75:hover {
--tw-bg-opacity: 0.75;
}
.hover\:text-accent-foreground:hover {
--tw-text-opacity: 1;
color: hsl(var(--accent-foreground) / var(--tw-text-opacity));
@ -1584,6 +1713,11 @@ body {
color: hsl(var(--primary) / 0.8);
}
.hover\:text-neutral-500:hover {
--tw-text-opacity: 1;
color: rgb(115 115 115 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
@ -1614,6 +1748,20 @@ body {
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
}
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.focus\:ring-ring:focus {
--tw-ring-opacity: 1;
--tw-ring-color: hsl(var(--ring) / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
.focus-visible\:outline-none:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;

View File

@ -27,6 +27,7 @@ func main() {
mux.Handle("GET /docs/components/card", templ.Handler(pages.Card()))
mux.Handle("GET /docs/components/input", templ.Handler(pages.Input()))
mux.Handle("GET /docs/components/accordion", templ.Handler(pages.Accordion()))
mux.Handle("GET /docs/components/datepicker", templ.Handler(pages.Datepicker()))
fmt.Println("Server is running on http://localhost:8090")
http.ListenAndServe(":8090", mux)

View File

@ -41,6 +41,10 @@ var Sections = []Section{
Text: "Card",
Href: "/docs/components/card",
},
{
Text: "Datepicker",
Href: "/docs/components/datepicker",
},
{
Text: "Input",
Href: "/docs/components/input",

View File

@ -0,0 +1,212 @@
package components
// DatepickerProps defines the properties for the Datepicker component.
type DatepickerProps struct {
// ID is the unique identifier for the datepicker input.
ID string
// Name is the name attribute for the datepicker input.
Name string
// Placeholder is the placeholder text for the datepicker input.
Placeholder string
// Format specifies the date format to use. Options: "M d, Y", "MM-DD-YYYY", "DD-MM-YYYY", "YYYY-MM-DD", "D d M, Y"
// Default: "M d, Y"
Format string
// Class specifies additional CSS classes to apply to the datepicker container.
Class string
// Attributes allows passing additional HTML attributes to the datepicker input element.
Attributes templ.Attributes
}
// Datepicker renders an enhanced datepicker component with an input field and a calendar view.
// It uses Alpine.js for interactivity and provides various formatting options and improved navigation.
//
// Usage:
//
// @components.Datepicker(components.DatepickerProps{
// ID: "my-datepicker",
// Name: "selected-date",
// Placeholder: "Select a date",
// Format: "YYYY-MM-DD",
// Class: "w-full",
// })
//
// Props:
// - ID: The unique identifier for the datepicker input. Default: "" (empty string)
// - Name: The name attribute for the datepicker input. Default: "" (empty string)
// - Placeholder: The placeholder text for the datepicker input. Default: "" (empty string)
// - Format: The date format to use. Default: "M d, Y"
// - Class: Additional CSS classes to apply to the datepicker container. Default: "" (empty string)
// - Attributes: Additional HTML attributes to apply to the datepicker input element. Default: nil
templ Datepicker(props DatepickerProps) {
<div
x-data="{
datePickerOpen: false,
datePickerValue: '',
datePickerFormat: '{ templ.EscapeString(props.Format) }',
datePickerMonth: '',
datePickerYear: '',
datePickerDay: '',
datePickerDaysInMonth: [],
datePickerBlankDaysInMonth: [],
datePickerMonthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
datePickerDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
datePickerDayClicked(day) {
let selectedDate = new Date(this.datePickerYear, this.datePickerMonth, day);
this.datePickerDay = day;
this.datePickerValue = this.datePickerFormatDate(selectedDate);
this.datePickerIsSelectedDate(day);
this.datePickerOpen = false;
},
datePickerPreviousMonth(){
if (this.datePickerMonth == 0) {
this.datePickerYear--;
this.datePickerMonth = 11;
} else {
this.datePickerMonth--;
}
this.datePickerCalculateDays();
},
datePickerNextMonth(){
if (this.datePickerMonth == 11) {
this.datePickerMonth = 0;
this.datePickerYear++;
} else {
this.datePickerMonth++;
}
this.datePickerCalculateDays();
},
datePickerIsSelectedDate(day) {
const d = new Date(this.datePickerYear, this.datePickerMonth, day);
return this.datePickerValue === this.datePickerFormatDate(d) ? true : false;
},
datePickerIsToday(day) {
const today = new Date();
const d = new Date(this.datePickerYear, this.datePickerMonth, day);
return today.toDateString() === d.toDateString() ? true : false;
},
datePickerCalculateDays() {
let daysInMonth = new Date(this.datePickerYear, this.datePickerMonth + 1, 0).getDate();
let dayOfWeek = new Date(this.datePickerYear, this.datePickerMonth).getDay();
let blankdaysArray = [];
for (var i = 1; i <= dayOfWeek; i++) {
blankdaysArray.push(i);
}
let daysArray = [];
for (var i = 1; i <= daysInMonth; i++) {
daysArray.push(i);
}
this.datePickerBlankDaysInMonth = blankdaysArray;
this.datePickerDaysInMonth = daysArray;
},
datePickerFormatDate(date) {
let formattedDay = this.datePickerDays[date.getDay()];
let formattedDate = ('0' + date.getDate()).slice(-2);
let formattedMonth = this.datePickerMonthNames[date.getMonth()];
let formattedMonthShortName = this.datePickerMonthNames[date.getMonth()].substring(0, 3);
let formattedMonthInNumber = ('0' + (parseInt(date.getMonth()) + 1)).slice(-2);
let formattedYear = date.getFullYear();
if (this.datePickerFormat === 'M d, Y') {
return `${formattedMonthShortName} ${formattedDate}, ${formattedYear}`;
}
if (this.datePickerFormat === 'MM-DD-YYYY') {
return `${formattedMonthInNumber}-${formattedDate}-${formattedYear}`;
}
if (this.datePickerFormat === 'DD-MM-YYYY') {
return `${formattedDate}-${formattedMonthInNumber}-${formattedYear}`;
}
if (this.datePickerFormat === 'YYYY-MM-DD') {
return `${formattedYear}-${formattedMonthInNumber}-${formattedDate}`;
}
if (this.datePickerFormat === 'D d M, Y') {
return `${formattedDay} ${formattedDate} ${formattedMonthShortName} ${formattedYear}`;
}
return `${formattedMonth} ${formattedDate}, ${formattedYear}`;
},
}"
x-init="
currentDate = new Date();
datePickerMonth = currentDate.getMonth();
datePickerYear = currentDate.getFullYear();
datePickerDay = currentDate.getDate();
datePickerValue = datePickerFormatDate(currentDate);
datePickerCalculateDays();
"
class={ "relative", props.Class }
x-cloak
>
<div class="relative">
<input
type="text"
id={ props.ID }
name={ props.Name }
placeholder={ props.Placeholder }
x-model="datePickerValue"
@click="datePickerOpen = !datePickerOpen"
x-on:keydown.escape="datePickerOpen = false"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
readonly
{ props.Attributes... }
/>
<div
@click="datePickerOpen = !datePickerOpen"
class="absolute top-0 right-0 px-3 py-2 cursor-pointer text-neutral-400 hover:text-neutral-500"
>
<svg class="w-6 h-6" 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>
</div>
</div>
<div
x-show="datePickerOpen"
x-transition
@click.away="datePickerOpen = false"
class="absolute top-0 left-0 max-w-lg p-4 mt-12 antialiased bg-white border rounded-lg shadow w-[17rem] border-neutral-200/70"
>
<div class="flex items-center justify-between mb-2">
<div>
<span x-text="datePickerMonthNames[datePickerMonth]" class="text-lg font-bold text-gray-800"></span>
<span x-text="datePickerYear" class="ml-1 text-lg font-normal text-gray-600"></span>
</div>
<div>
<button @click="datePickerPreviousMonth()" type="button" class="inline-flex p-1 transition duration-100 ease-in-out rounded-full cursor-pointer focus:outline-none focus:shadow-outline hover:bg-gray-100">
<svg class="inline-flex w-6 h-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
</button>
<button @click="datePickerNextMonth()" type="button" class="inline-flex p-1 transition duration-100 ease-in-out rounded-full cursor-pointer focus:outline-none focus:shadow-outline hover:bg-gray-100">
<svg class="inline-flex w-6 h-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</button>
</div>
</div>
<div class="grid grid-cols-7 mb-3">
<template x-for="(day, index) in datePickerDays" :key="index">
<div class="px-0.5">
<div x-text="day" class="text-xs font-medium text-center text-gray-800"></div>
</div>
</template>
</div>
<div class="grid grid-cols-7">
<template x-for="blankDay in datePickerBlankDaysInMonth">
<div class="p-1 text-sm text-center border border-transparent"></div>
</template>
<template x-for="(day, dayIndex) in datePickerDaysInMonth" :key="dayIndex">
<div class="px-0.5 mb-1 aspect-square">
<div
x-text="day"
@click="datePickerDayClicked(day)"
:class="{
'bg-neutral-200': datePickerIsToday(day) == true,
'text-gray-600 hover:bg-neutral-200': datePickerIsToday(day) == false && datePickerIsSelectedDate(day) == false,
'bg-neutral-800 text-white hover:bg-opacity-75': datePickerIsSelectedDate(day) == true
}"
class="flex items-center justify-center text-sm leading-none text-center rounded-full cursor-pointer h-7 w-7"
></div>
</div>
</template>
</div>
</div>
</div>
}

View File

@ -0,0 +1,39 @@
package pages
import (
"github.com/axzilla/goilerplate/internals/ui/components"
"github.com/axzilla/goilerplate/internals/ui/layouts"
"github.com/axzilla/goilerplate/internals/ui/showcase"
)
templ Datepicker() {
@layouts.DocsLayout() {
<div>
<div class="mb-16">
<h1 class="text-3xl font-bold mb-2">Datepicker</h1>
<p class="mb-4 text-muted-foreground">A date picker component.</p>
</div>
@components.Tabs(components.TabsProps{
Tabs: []components.Tab{
{
ID: "preview",
Title: "Preview",
Content: showcase.DatepickerShowcase(),
},
{
ID: "code",
Title: "Code",
Content: CodeSnippetFromEmbedded("datepicker.templ", "go", showcase.TemplFiles),
},
{
ID: "component",
Title: "Component",
Content: CodeSnippetFromEmbedded("datepicker.templ", "go", components.TemplFiles),
},
},
TabsContainerClass: "md:w-1/2",
ContentContainerClass: "w-full",
})
</div>
}
}

View File

@ -0,0 +1,17 @@
package showcase
import (
"github.com/axzilla/goilerplate/internals/ui/components"
)
templ DatepickerShowcase() {
<div class="flex justify-center items-center border rounded-md py-16 px-4">
@components.Datepicker(components.DatepickerProps{
ID: "my-datepicker",
Name: "selected-date",
Placeholder: "Select a date",
Format: "YYYY-MM-DD",
Class: "w-full max-w-xs",
})
</div>
}