Add date picker to project form
This commit is contained in:
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ALTER COLUMN "date" DROP DEFAULT;
|
@ -69,7 +69,7 @@ model Project {
|
||||
title String
|
||||
description String @default("No description provided")
|
||||
images ProjectImage[]
|
||||
date DateTime @default(now())
|
||||
date DateTime
|
||||
links String[] @default([])
|
||||
tags Tag[]
|
||||
}
|
||||
|
@ -27,6 +27,7 @@
|
||||
"@uppy/progress-bar": "^4.0.0",
|
||||
"@uppy/react": "^4.0.1",
|
||||
"@uppy/tus": "^4.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"humanize-string": "2.1.0",
|
||||
"react": "18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
|
103
web/src/components/DatePicker/DatePicker.tsx
Normal file
103
web/src/components/DatePicker/DatePicker.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import {
|
||||
add,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
format,
|
||||
getDay,
|
||||
isEqual,
|
||||
isSameMonth,
|
||||
isToday,
|
||||
parse,
|
||||
startOfWeek,
|
||||
sub,
|
||||
} from 'date-fns'
|
||||
|
||||
interface DatePickerProps {
|
||||
date: Date
|
||||
setDate: React.Dispatch<React.SetStateAction<Date>>
|
||||
month: string
|
||||
setMonth: React.Dispatch<React.SetStateAction<string>>
|
||||
}
|
||||
|
||||
const DatePicker = ({ date, setDate, month, setMonth }: DatePickerProps) => {
|
||||
const currentMonthFirstDay = parse(month, 'MMMM yyyy', new Date())
|
||||
|
||||
const days = eachDayOfInterval({
|
||||
start: startOfWeek(currentMonthFirstDay),
|
||||
end: endOfWeek(endOfMonth(currentMonthFirstDay)),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="max-w-fit bg-base-100 space-y-2 p-2 rounded-xl">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setMonth(
|
||||
format(sub(currentMonthFirstDay, { months: 1 }), 'MMMM yyyy')
|
||||
)
|
||||
}
|
||||
className="btn btn-sm btn-square btn-ghost"
|
||||
>
|
||||
<Icon path={mdiChevronLeft} className="size-5" />
|
||||
</button>
|
||||
|
||||
<p>{format(currentMonthFirstDay, 'MMMM yyyy')}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setMonth(
|
||||
format(add(currentMonthFirstDay, { months: 1 }), 'MMMM yyyy')
|
||||
)
|
||||
}
|
||||
className="btn btn-sm btn-square btn-ghost"
|
||||
>
|
||||
<Icon path={mdiChevronRight} className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-2 border-y py-1 border-base-300 grid-cols-7 text-center">
|
||||
{['S', 'M', 'T', 'W', 'T', 'F', 'S '].map((weekday, i) => (
|
||||
<div key={i}>{weekday}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-2 grid-cols-7 text-center">
|
||||
{days.map((day, i) => {
|
||||
const selected = isEqual(day, date)
|
||||
const isCurrentMonth = isSameMonth(day, currentMonthFirstDay)
|
||||
const today = isToday(day)
|
||||
const weekday = getDay(day)
|
||||
|
||||
let btnColor = 'btn-ghost'
|
||||
|
||||
if (!isCurrentMonth) btnColor += ' opacity-40'
|
||||
|
||||
if (today) btnColor = 'btn-error'
|
||||
if (selected) btnColor = 'btn-primary'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toString()}
|
||||
className={`${i === 0 && ['col-start-1', 'col-start-2', 'col-start-3', 'col-start-4', 'col-start-5', 'col-start-6', 'col-start-7'][weekday]}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDate(day)}
|
||||
className={`btn btn-sm btn-square ${btnColor}`}
|
||||
>
|
||||
<time dateTime={format(day, 'yyyy-MM-dd')}>
|
||||
{format(day, 'd')}
|
||||
</time>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatePicker
|
@ -74,9 +74,15 @@ const Project = ({ project }: Props) => {
|
||||
<th>Links</th>
|
||||
<td className="space-x-2 space-y-2">
|
||||
{project.links.map((link, i) => (
|
||||
<div className="badge badge-ghost text-nowrap" key={i}>
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="badge badge-ghost text-nowrap"
|
||||
key={i}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { mdiFormatTitle, mdiLinkVariant } from '@mdi/js'
|
||||
import { mdiCalendar, mdiFormatTitle, mdiLinkVariant } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import { format, isAfter, startOfToday } from 'date-fns'
|
||||
import type { EditProjectById, UpdateProjectInput } from 'types/graphql'
|
||||
|
||||
import type { RWGqlError } from '@redwoodjs/forms'
|
||||
@ -15,6 +16,7 @@ import {
|
||||
} from '@redwoodjs/forms'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
|
||||
import DatePicker from 'src/components/DatePicker/DatePicker'
|
||||
import FormTextList from 'src/components/FormTextList/FormTextList'
|
||||
|
||||
type FormProject = NonNullable<EditProjectById['project']>
|
||||
@ -29,8 +31,15 @@ interface ProjectFormProps {
|
||||
}
|
||||
|
||||
const ProjectForm = (props: ProjectFormProps) => {
|
||||
const today = startOfToday()
|
||||
|
||||
const [links, setLinks] = useState<string[]>(props.project?.links || [])
|
||||
const [linkErrors, setLinkErrors] = useState<boolean[]>([])
|
||||
const [pickerVisible, setPickerVisible] = useState<boolean>(false)
|
||||
const [date, setDate] = useState<Date>(
|
||||
props.project?.date ? new Date(props.project.date) : today
|
||||
)
|
||||
const [month, setMonth] = useState<string>(format(today, 'MMMM yyyy'))
|
||||
|
||||
const urlRegex = useMemo(
|
||||
() =>
|
||||
@ -51,7 +60,7 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
if (emptyCount > 0) return toast.error(`${emptyCount} links empty`)
|
||||
|
||||
data.links = links
|
||||
data.date = new Date().toISOString() // TODO: change to date picker value
|
||||
data.date = date.toISOString()
|
||||
props.onSave(data, props?.project?.id)
|
||||
}
|
||||
|
||||
@ -115,6 +124,42 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<Label
|
||||
name="date"
|
||||
className="input input-bordered flex items-center gap-2"
|
||||
errorClassName="input input-bordered flex items-center gap-2 input-error"
|
||||
onClick={() => setPickerVisible(!pickerVisible)}
|
||||
>
|
||||
<Label
|
||||
name="date"
|
||||
className="size-4 opacity-70"
|
||||
errorClassName="size-4 text-error"
|
||||
>
|
||||
<Icon path={mdiCalendar} />
|
||||
</Label>
|
||||
<TextField
|
||||
name="date"
|
||||
ref={titleRef}
|
||||
placeholder="Date"
|
||||
value={format(date, 'PP')}
|
||||
className="w-full"
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{pickerVisible && (
|
||||
<div className="flex justify-center">
|
||||
<DatePicker
|
||||
date={date}
|
||||
setDate={setDate}
|
||||
month={month}
|
||||
setMonth={setMonth}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${!pickerVisible && 'pt-2'}`}>
|
||||
<FormTextList
|
||||
name="Links"
|
||||
itemPlaceholder="URL"
|
||||
@ -123,6 +168,14 @@ const ProjectForm = (props: ProjectFormProps) => {
|
||||
errors={linkErrors}
|
||||
setList={setLinks}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isAfter(date, today) && (
|
||||
<div className="flex justify-center py-2">
|
||||
<p>Project will be marked as</p>
|
||||
<div className="ml-1 badge badge-info">planned</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="my-2 flex justify-center space-x-2">
|
||||
<Submit
|
||||
|
@ -86,9 +86,15 @@ const ProjectsList = ({ projects }: FindProjects) => {
|
||||
<td>{timeTag(project.date)}</td>
|
||||
<td className="space-x-2 space-y-2">
|
||||
{project.links.map((link, i) => (
|
||||
<div className="badge badge-ghost text-nowrap" key={i}>
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="badge badge-ghost text-nowrap"
|
||||
key={i}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import { format } from 'date-fns'
|
||||
import humanize from 'humanize-string'
|
||||
|
||||
const MAX_STRING_LENGTH = 150
|
||||
@ -41,11 +42,12 @@ export const jsonTruncate = (obj: unknown) => {
|
||||
|
||||
export const timeTag = (dateTime?: string) => {
|
||||
let output: string | JSX.Element = ''
|
||||
const date = new Date(dateTime)
|
||||
|
||||
if (dateTime) {
|
||||
output = (
|
||||
<time dateTime={dateTime} title={dateTime}>
|
||||
{new Date(dateTime).toUTCString()}
|
||||
{format(date, 'PPpp')}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
@ -8821,6 +8821,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "date-fns@npm:4.1.0"
|
||||
checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debounce@npm:^1.2.0":
|
||||
version: 1.2.1
|
||||
resolution: "debounce@npm:1.2.1"
|
||||
@ -18243,6 +18250,7 @@ __metadata:
|
||||
"@uppy/tus": "npm:^4.0.0"
|
||||
autoprefixer: "npm:^10.4.20"
|
||||
daisyui: "npm:^4.12.10"
|
||||
date-fns: "npm:^4.1.0"
|
||||
humanize-string: "npm:2.1.0"
|
||||
postcss: "npm:^8.4.41"
|
||||
postcss-loader: "npm:^8.1.1"
|
||||
|
Reference in New Issue
Block a user