広場投稿追加画面の刷新 (#399) (#413)

Reviewed-on: http://git.miteruzo.com/miteruzo/btrc-hub/pulls/413
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #413 でマージされました。
このコミットが含まれているのは:
2026-07-19 00:03:10 +09:00
committed by みてるぞ
コミット f1181e8510
99個のファイルの変更、10242行の追加、1126行の削除
+2 -1
ファイルの表示
@@ -21,7 +21,8 @@ describe ('DateTimeField', () => {
fireEvent.change (input, { target: { value: '' } })
const first = handleChange.mock.calls[0]?.[0]
expect (new Date (first).getFullYear ()).toBe (2026)
expect (first).toMatch (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/)
expect (new Date (first).getUTCSeconds ()).toBe (0)
expect (handleChange).toHaveBeenLastCalledWith (null)
})
})
+24 -8
ファイルの表示
@@ -8,13 +8,27 @@ import type { ComponentPropsWithoutRef, FC, FocusEvent } from 'react'
const pad = (n: number): string => n.toString ().padStart (2, '0')
const toDateTimeLocalValue = (d: Date) => {
const y = d.getFullYear ()
const m = pad (d.getMonth () + 1)
const day = pad (d.getDate ())
const h = pad (d.getHours ())
const min = pad (d.getMinutes ())
return `${ y }-${ m }-${ day }T${ h }:${ min }:00`
const toDateTimeLocalValue = (value: Date) => {
const y = value.getFullYear ()
const m = pad (value.getMonth () + 1)
const day = pad (value.getDate ())
const h = pad (value.getHours ())
const min = pad (value.getMinutes ())
return `${ y }-${ m }-${ day }T${ h }:${ min }`
}
const toMinutePrecisionIsoUtc = (value: string) => {
const date = new Date (value)
if (Number.isNaN (date.getTime ()))
return value
const y = date.getUTCFullYear ()
const m = pad (date.getUTCMonth () + 1)
const day = pad (date.getUTCDate ())
const h = pad (date.getUTCHours ())
const min = pad (date.getUTCMinutes ())
return `${ y }-${ m }-${ day }T${ h }:${ min }Z`
}
@@ -47,14 +61,16 @@ const DateTimeField: FC<Props> = ({ value, onChange, className, onBlur, invalid,
'focus:ring-2 focus:ring-blue-200']),
className)}
type="datetime-local"
step={60}
value={local}
aria-invalid={invalid}
onChange={ev => {
const v = ev.target.value
setLocal (v)
onChange?.(v ? (new Date (v)).toISOString () : null)
onChange?.(v ? toMinutePrecisionIsoUtc (v) : null)
}}
onBlur={onBlur}/>)
}
export default DateTimeField
export { toMinutePrecisionIsoUtc }
+18
ファイルの表示
@@ -0,0 +1,18 @@
import type { FC } from 'react'
type Props = { id?: string
messages?: string[] }
export const FieldWarning: FC<Props> = ({ id, messages }: Props) => {
if (messages == null || messages.length === 0)
return null
return (
<ul id={id} className="mt-1 space-y-1 text-amber-700 dark:text-amber-200">
{messages.map ((message, i) => <li key={i}>{message}</li>)}
</ul>)
}
export default FieldWarning
+8 -4
ファイルの表示
@@ -1,11 +1,15 @@
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
type Props = { children: ReactNode }
type Props = {
children: ReactNode
className?: string }
const Form: FC<Props> = ({ children }) => (
<div className="max-w-xl mx-auto p-4 space-y-4">
const Form: FC<Props> = ({ children, className }) => (
<div className={cn ('mx-auto max-w-xl space-y-4 p-4', className)}>
{children}
</div>)
export default Form
export default Form
+34
ファイルの表示
@@ -0,0 +1,34 @@
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
export type StatusBadgeTone =
'success'
| 'neutral'
| 'warning'
type Props = {
children: ReactNode
tone: StatusBadgeTone }
const TONES: Record<StatusBadgeTone, string[]> = {
success: [
'border-emerald-300 bg-emerald-50 text-emerald-700',
'dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200'],
warning: [
'border-amber-300 bg-amber-50 text-amber-700',
'dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200'],
neutral: [
'border-stone-300 bg-stone-50 text-stone-700',
'dark:border-stone-700 dark:bg-stone-900 dark:text-stone-200'] }
const StatusBadge: FC<Props> = ({ children, tone }) => (
<span
className={cn (
'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium',
TONES[tone])}>
{children}
</span>)
export default StatusBadge