This commit is contained in:
2026-05-10 05:03:27 +09:00
parent de86879e79
commit 5b50642756
12 changed files with 185 additions and 122 deletions
+26 -11
View File
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { updatePost } from '@/lib/posts'
import type { FC } from 'react'
import type { FC, FormEvent } from 'react'
import type { Post, Tag } from '@/types'
@@ -33,6 +33,7 @@ type Props = { post: Post
export default (({ post, onSave }: Props) => {
const [disabled, setDisabled] = useState (false)
const [originalCreatedBefore, setOriginalCreatedBefore] =
useState<string | null> (post.originalCreatedBefore)
const [originalCreatedFrom, setOriginalCreatedFrom] =
@@ -61,7 +62,9 @@ export default (({ post, onSave }: Props) => {
}
catch (e)
{
if (e.response.status !== 409)
const response = (e as any)?.response
if (response?.status !== 409)
{
toast ({ description: '更新はできなかったよ……' })
return
@@ -74,7 +77,7 @@ export default (({ post, onSave }: Props) => {
<p></p>
<p>?</p>
</div>),
choices: [{ value: 'merge', label: '差分をマージ' },
choices: [...(response?.data?.mergeable ? [{ value: 'merge', label: '差分をマージ' }] : []),
{ value: 'overwrite', label: '強制上書き', variant: 'danger' }] })
if (action === 'merge')
@@ -96,12 +99,14 @@ export default (({ post, onSave }: Props) => {
}
}
const handleSubmit = async e => {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault ()
setDisabled (true)
await update ({ id: post.id, title, tags, parentPostIds,
originalCreatedFrom, originalCreatedBefore },
{ baseVersionNo: post.versionNo })
setDisabled (false)
}
useEffect (() => {
@@ -113,10 +118,12 @@ export default (({ post, onSave }: Props) => {
{/* タイトル */}
<div>
<Label></Label>
<input type="text"
className="w-full border rounded p-2"
value={title ?? ''}
onChange={ev => setTitle (ev.target.value)}/>
<input
type="text"
disabled={disabled}
className="w-full border rounded p-2"
value={title ?? ''}
onChange={ev => setTitle (ev.target.value)}/>
</div>
{/* 親投稿 */}
@@ -124,24 +131,32 @@ export default (({ post, onSave }: Props) => {
<Label>稿</Label>
<input
type="text"
disabled={disabled}
value={parentPostIds}
onChange={e => setParentPostIds (e.target.value)}
className="w-full border p-2 rounded"/>
</div>
{/* タグ */}
<PostFormTagsArea tags={tags} setTags={setTags}/>
<PostFormTagsArea
disabled={disabled}
tags={tags}
setTags={setTags}/>
{/* オリジナルの作成日時 */}
<PostOriginalCreatedTimeField
disabled={disabled}
originalCreatedFrom={originalCreatedFrom}
setOriginalCreatedFrom={setOriginalCreatedFrom}
originalCreatedBefore={originalCreatedBefore}
setOriginalCreatedBefore={setOriginalCreatedBefore}/>
{/* 送信 */}
<Button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
<Button
type="submit"
disabled={disabled}
onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
</Button>
</div>)
+8 -6
View File
@@ -7,7 +7,7 @@ import Label from '@/components/common/Label'
import TextArea from '@/components/common/TextArea'
import { apiGet } from '@/lib/api'
import type { FC, SyntheticEvent } from 'react'
import type { ComponentPropsWithoutRef, FC, SyntheticEvent } from 'react'
import type { Tag } from '@/types'
@@ -31,12 +31,13 @@ const replaceToken = (value: string, start: number, end: number, text: string) =
`${ value.slice (0, start) }${ text }${ value.slice (end) }`
type Props = {
tags: string
setTags: (tags: string) => void }
type Props =
& { tags: string
setTags: (tags: string) => void }
& ComponentPropsWithoutRef<'textarea'>
export default (({ tags, setTags }: Props) => {
export default (({ tags, setTags, ...rest }: Props) => {
const ref = useRef<HTMLTextAreaElement> (null)
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
@@ -87,7 +88,8 @@ export default (({ tags, setTags }: Props) => {
onBlur={() => {
setFocused (false)
setSuggestionsVsbl (false)
}}/>
}}
{...rest}/>
{focused && (
<TagSearchBox
suggestions={suggestionsVsbl && suggestions.length > 0
@@ -5,13 +5,15 @@ import { Button } from '@/components/ui/button'
import type { FC } from 'react'
type Props = {
disabled?: boolean
originalCreatedFrom: string | null
setOriginalCreatedFrom: (x: string | null) => void
originalCreatedBefore: string | null
setOriginalCreatedBefore: (x: string | null) => void }
export default (({ originalCreatedFrom,
export default (({ disabled,
originalCreatedFrom,
setOriginalCreatedFrom,
originalCreatedBefore,
setOriginalCreatedBefore }: Props) => (
@@ -21,6 +23,7 @@ export default (({ originalCreatedFrom,
<div className="w-80">
<DateTimeField
className="mr-2"
disabled={disabled ?? false}
value={originalCreatedFrom ?? undefined}
onChange={setOriginalCreatedFrom}
onBlur={ev => {
@@ -40,6 +43,7 @@ export default (({ originalCreatedFrom,
<div>
<Button
className="bg-gray-600 text-white rounded"
disabled={disabled}
onClick={() => {
setOriginalCreatedFrom (null)
}}>
@@ -51,6 +55,7 @@ export default (({ originalCreatedFrom,
<div className="w-80">
<DateTimeField
className="mr-2"
disabled={disabled}
value={originalCreatedBefore ?? undefined}
onChange={setOriginalCreatedBefore}/>
@@ -58,6 +63,7 @@ export default (({ originalCreatedFrom,
<div>
<Button
className="bg-gray-600 text-white rounded"
disabled={disabled}
onClick={() => {
setOriginalCreatedBefore (null)
}}>
+3 -3
View File
@@ -36,12 +36,12 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
{ name: '一覧', to: '/posts' },
{ name: '検索', to: '/posts/search' },
{ name: '追加', to: '/posts/new' },
{ name: '履歴', to: '/posts/changes' },
{ name: '全体履歴', to: '/posts/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
{ name: 'タグ', to: '/tags', subMenu: [
{ name: 'マスタ', to: '/tags' },
{ name: 'ニコニコ連携', to: '/tags/nico' },
{ name: '履歴', to: '/tags/changes' },
{ name: '全体履歴', to: '/tags/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
{ component: <Separator/>, visible: tagFlg },
{ name: `広場 (${ postCount || 0 })`,
@@ -53,7 +53,7 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
{ name: '一覧', to: '/materials' },
{ name: '検索', to: '/materials/search', visible: false },
{ name: '追加', to: '/materials/new' },
{ name: '履歴', to: '/materials/changes', visible: false },
{ name: '全体履歴', to: '/materials/changes', visible: false },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] },
{ name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [
{ name: <>&thinsp;1&thinsp;</>, to: '/theatres/1' },
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
import type { FC, FocusEvent } from 'react'
import type { ComponentPropsWithoutRef, FC, FocusEvent } from 'react'
const pad = (n: number): string => n.toString ().padStart (2, '0')
@@ -18,14 +18,14 @@ const toDateTimeLocalValue = (d: Date) => {
}
type Props = {
type Props = Omit<ComponentPropsWithoutRef<'input'>, 'onChange'> & {
value?: string
onChange?: (isoUTC: string | null) => void
className?: string
onBlur?: (ev: FocusEvent<HTMLInputElement>) => void }
export default (({ value, onChange, className, onBlur }: Props) => {
export default (({ value, onChange, className, onBlur, ...rest }: Props) => {
const [local, setLocal] = useState ('')
useEffect (() => {
@@ -42,5 +42,6 @@ export default (({ value, onChange, className, onBlur }: Props) => {
setLocal (v)
onChange?.(v ? (new Date (v)).toISOString () : null)
}}
onBlur={onBlur}/>)
onBlur={onBlur}
{...rest}/>)
}) satisfies FC<Props>
+13 -11
View File
@@ -37,25 +37,27 @@ const DialogContent = React.forwardRef<
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 w-[90%] grid max-w-lg',
className={cn (
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-lg',
'translate-x-[-50%] translate-y-[-50%]',
'gap-4 border bg-gray-300/80 dark:bg-gray-700/80',
'p-6 shadow-lg duration-200',
'gap-5 rounded-2xl border border-border',
'bg-background p-6 text-foreground shadow-2xl',
'duration-200',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[state=closed]:slide-out-to-left-1/2',
'data-[state=closed]:slide-out-to-top-[48%]',
'data-[state=open]:slide-in-from-left-1/2',
'data-[state=open]:slide-in-from-top-[48%] rounded-lg',
className)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 bg-red-500 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-3 w-3" />
<span className="sr-only">Close</span>
<DialogPrimitive.Close
className={cn (
'absolute right-4 top-4 rounded-full p-1',
'text-muted-foreground opacity-70 transition-opacity',
'hover:bg-accent hover:text-accent-foreground hover:opacity-100',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2')}>
<X className="h-4 w-4"/>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
+50 -28
View File
@@ -6,6 +6,56 @@
@layer base
{
:root
{
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
}
.dark
{
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 62.8% 45%;
--destructive-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
body
{
@apply overflow-x-clip;
@@ -54,34 +104,6 @@ body
min-height: 100dvh;
}
h1
{
font-size: 3.2em;
line-height: 1.1;
}
button
{
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover
{
border-color: #646cff;
}
button:focus,
button:focus-visible
{
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light)
{
:root
+16 -1
View File
@@ -19,7 +19,22 @@ export default {
'rainbow-scroll': 'rainbow-scroll .25s linear infinite' },
colors: {
red: { 925: '#5f1414',
975: '#230505' } },
975: '#230505' },
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: { DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))' },
secondary: { DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))' },
destructive: { DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))' },
muted: { DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))' },
accent: { DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))' } },
keyframes: {
'rainbow-scroll': {
'0%': { backgroundPosition: '0% 50%' },