This commit is contained in:
@@ -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>)
|
||||
|
||||
@@ -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)
|
||||
}}>
|
||||
|
||||
@@ -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: <>第 1 会場</>, 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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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%' },
|
||||
|
||||
Reference in New Issue
Block a user