#171 #171 #171 #171 #171 #171 #171 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #345
This commit was merged in pull request #345.
This commit is contained in:
@@ -3,11 +3,12 @@ import { useEffect, useState } from 'react'
|
||||
import PostFormTagsArea from '@/components/PostFormTagsArea'
|
||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||
import Label from '@/components/common/Label'
|
||||
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { apiPut } from '@/lib/api'
|
||||
import { updatePost } from '@/lib/posts'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, FormEvent } from 'react'
|
||||
|
||||
import type { Post, Tag } from '@/types'
|
||||
|
||||
@@ -32,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] =
|
||||
@@ -41,16 +43,14 @@ export default (({ post, onSave }: Props) => {
|
||||
const [tags, setTags] = useState<string> ('')
|
||||
const [title, setTitle] = useState (post.title)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const dialogue = useDialogue ()
|
||||
|
||||
const update = async (...args: Parameters<typeof updatePost>) => {
|
||||
try
|
||||
{
|
||||
const data = await apiPut<Post> (
|
||||
`/posts/${ post.id }`,
|
||||
{ title, tags, parent_post_ids: parentPostIds,
|
||||
original_created_from: originalCreatedFrom,
|
||||
original_created_before: originalCreatedBefore },
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
const data = await updatePost (...args)
|
||||
onSave ({ ...post,
|
||||
versionNo: data.versionNo,
|
||||
title: data.title,
|
||||
tags: data.tags,
|
||||
parentPosts: data.parentPosts,
|
||||
@@ -60,9 +60,58 @@ export default (({ post, onSave }: Props) => {
|
||||
originalCreatedBefore: data.originalCreatedBefore } as Post)
|
||||
toast ({ description: '更新しました.' })
|
||||
}
|
||||
catch
|
||||
catch (e)
|
||||
{
|
||||
toast ({ description: '更新はできなかったよ……' })
|
||||
const response = (e as any)?.response
|
||||
|
||||
if (response?.status !== 409)
|
||||
{
|
||||
toast ({ description: '更新はできなかったよ……' })
|
||||
return
|
||||
}
|
||||
|
||||
const action = await dialogue.choice ({
|
||||
title: '競合が発生しました.',
|
||||
description: (
|
||||
<div>
|
||||
<p>ほかの耕作員が先に更新してゐます.</p>
|
||||
<p>現在の変更をどう扱ひますか?</p>
|
||||
</div>),
|
||||
choices: [...(response?.data?.mergeable ? [{ value: 'merge', label: '差分をマージ' }] : []),
|
||||
{ value: 'overwrite', label: '強制上書き', variant: 'danger' }] })
|
||||
|
||||
if (action === 'merge')
|
||||
{
|
||||
// TODO: 差分 UI
|
||||
await update ({ id: post.id, title, tags, parentPostIds,
|
||||
originalCreatedFrom, originalCreatedBefore },
|
||||
{ baseVersionNo: post.versionNo, merge: true })
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'overwrite')
|
||||
{
|
||||
await update ({ id: post.id, title, tags, parentPostIds,
|
||||
originalCreatedFrom, originalCreatedBefore },
|
||||
{ baseVersionNo: post.versionNo, force: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault ()
|
||||
|
||||
setDisabled (true)
|
||||
try
|
||||
{
|
||||
await update ({ id: post.id, title, tags, parentPostIds,
|
||||
originalCreatedFrom, originalCreatedBefore },
|
||||
{ baseVersionNo: post.versionNo })
|
||||
}
|
||||
finally
|
||||
{
|
||||
setDisabled (false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +120,16 @@ export default (({ post, onSave }: Props) => {
|
||||
}, [post])
|
||||
|
||||
return (
|
||||
<div className="max-w-xl pt-2 space-y-4">
|
||||
<form onSubmit={handleSubmit} className="max-w-xl pt-2 space-y-4">
|
||||
{/* タイトル */}
|
||||
<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>
|
||||
|
||||
{/* 親投稿 */}
|
||||
@@ -86,25 +137,31 @@ 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}>
|
||||
更新
|
||||
</Button>
|
||||
</div>)
|
||||
</form>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
@@ -3,6 +3,7 @@ import YoutubeEmbed from 'react-youtube'
|
||||
|
||||
import NicoViewer from '@/components/NicoViewer'
|
||||
import TwitterEmbed from '@/components/TwitterEmbed'
|
||||
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||
|
||||
import type { FC, RefObject } from 'react'
|
||||
|
||||
@@ -16,6 +17,8 @@ type Props = {
|
||||
|
||||
|
||||
export default (({ ref, post, onLoadComplete, onMetadataChange }: Props) => {
|
||||
const dialogue = useDialogue ()
|
||||
|
||||
const url = new URL (post.url)
|
||||
|
||||
switch (url.hostname.split ('.').slice (-2).join ('.'))
|
||||
@@ -82,12 +85,17 @@ export default (({ ref, post, onLoadComplete, onMetadataChange }: Props) => {
|
||||
height={360}/>)
|
||||
: (
|
||||
<div>
|
||||
<a href="#" onClick={e => {
|
||||
<a href="#" onClick={async e => {
|
||||
e.preventDefault ()
|
||||
setFramed (confirm ('未確認の外部ページを表示します。\n'
|
||||
+ '悪意のあるスクリプトが実行される可能性があります。\n'
|
||||
+ '表示しますか?'))
|
||||
return
|
||||
|
||||
setFramed (await dialogue.confirm ({
|
||||
title: '未確認の外部ページを表示します。',
|
||||
description: (
|
||||
<div>
|
||||
<p>悪意のあるスクリプトが実行される可能性があります。</p>
|
||||
<p>表示しますか?</p>
|
||||
</div>),
|
||||
confirmText: '表示' }))
|
||||
}}>
|
||||
外部ページを表示
|
||||
</a>
|
||||
|
||||
@@ -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,12 @@ const replaceToken = (value: string, start: number, end: number, text: string) =
|
||||
`${ value.slice (0, start) }${ text }${ value.slice (end) }`
|
||||
|
||||
|
||||
type Props = {
|
||||
type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | 'onBlur'> & {
|
||||
tags: string
|
||||
setTags: (tags: string) => void }
|
||||
|
||||
|
||||
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 })
|
||||
@@ -76,6 +76,7 @@ export default (({ tags, setTags }: Props) => {
|
||||
<div className="relative w-full">
|
||||
<Label>タグ</Label>
|
||||
<TextArea
|
||||
{...rest}
|
||||
ref={ref}
|
||||
value={tags}
|
||||
onChange={ev => setTags (ev.target.value)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export default (({ posts, onClick }: Props) => {
|
||||
layoutId={layoutId}
|
||||
className={cn ('w-full h-full overflow-hidden rounded-xl shadow',
|
||||
'transform-gpu will-change-transform',
|
||||
(post.childPosts ?? []).length > 0 && 'outline-4 outline-green-500',
|
||||
(post.childPosts ?? []).length > 0 && 'ring-4 ring-green-500',
|
||||
(post.parentPosts ?? []).length > 0 && 'ring-4 ring-yellow-500')}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onLayoutAnimationStart={() => {
|
||||
|
||||
@@ -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 (() => {
|
||||
@@ -34,6 +34,7 @@ export default (({ value, onChange, className, onBlur }: Props) => {
|
||||
|
||||
return (
|
||||
<input
|
||||
{...rest}
|
||||
className={cn ('border rounded p-2', className)}
|
||||
type="datetime-local"
|
||||
value={local}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle } from '@/components/ui/dialog'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
type DialogueVariant = 'default' | 'danger'
|
||||
|
||||
type ConfirmOptions = { title: string
|
||||
description?: ReactNode
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
variant?: DialogueVariant }
|
||||
|
||||
type AlertOptions = { title: string
|
||||
description?: ReactNode
|
||||
okText?: string }
|
||||
|
||||
type Choice<T extends string> = { value: T
|
||||
label: string
|
||||
variant?: DialogueVariant }
|
||||
|
||||
type ChoiceOptions<T extends string> = { title: string
|
||||
description?: ReactNode
|
||||
choices: Choice<T>[]
|
||||
cancelText?: string }
|
||||
|
||||
type DialogueRequest =
|
||||
| { id: number
|
||||
kind: 'confirm'
|
||||
options: ConfirmOptions
|
||||
resolve: (value: boolean) => void }
|
||||
| { id: number
|
||||
kind: 'alert'
|
||||
options: AlertOptions
|
||||
resolve: () => void }
|
||||
| { id: number
|
||||
kind: 'choice'
|
||||
options: ChoiceOptions<string>
|
||||
resolve: (value: string | null) => void }
|
||||
|
||||
type DialogueAPI =
|
||||
{ confirm: (options: ConfirmOptions) => Promise<boolean>
|
||||
alert: (options: AlertOptions) => Promise<void>
|
||||
choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null> }
|
||||
|
||||
const DialogueContext = createContext<DialogueAPI | null> (null)
|
||||
|
||||
let nextDialogueId = 1
|
||||
|
||||
type Props = { children: ReactNode }
|
||||
|
||||
|
||||
export default (({ children }: Props) => {
|
||||
const [queue, setQueue] = useState<DialogueRequest[]> ([])
|
||||
|
||||
const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
|
||||
const id = nextDialogueId
|
||||
++nextDialogueId
|
||||
|
||||
setQueue (q => [...q, { ...request, id } as DialogueRequest])
|
||||
}, [])
|
||||
|
||||
const closeActive = useCallback ((result?: unknown) => {
|
||||
setQueue (q => {
|
||||
const [active, ...rest] = q
|
||||
|
||||
if (!(active))
|
||||
return rest
|
||||
|
||||
switch (active.kind)
|
||||
{
|
||||
case 'confirm':
|
||||
active.resolve (Boolean (result))
|
||||
break
|
||||
|
||||
case 'alert':
|
||||
active.resolve ()
|
||||
break
|
||||
|
||||
case 'choice':
|
||||
active.resolve ((result ?? null) as string | null)
|
||||
break
|
||||
}
|
||||
|
||||
return rest
|
||||
})
|
||||
}, [])
|
||||
|
||||
const api = useMemo<DialogueAPI> (() => ({
|
||||
confirm: options => new Promise<boolean> (resolve => {
|
||||
push ({ kind: 'confirm', options, resolve })
|
||||
}),
|
||||
alert: options => new Promise<void> (resolve => {
|
||||
push ({ kind: 'alert', options, resolve })
|
||||
}),
|
||||
choice: options => new Promise (resolve => {
|
||||
push ({ kind: 'choice',
|
||||
options: options as ChoiceOptions<string>,
|
||||
resolve: resolve as (value: string | null) => void })
|
||||
}) }), [push])
|
||||
|
||||
const active = queue[0]
|
||||
|
||||
return (
|
||||
<DialogueContext.Provider value={api}>
|
||||
{children}
|
||||
|
||||
<Dialog
|
||||
open={Boolean (active)}
|
||||
onOpenChange={open => {
|
||||
if (!(open))
|
||||
closeActive (active?.kind !== 'confirm' && null)
|
||||
}}>
|
||||
{active && (
|
||||
<DialogContent className="px-6 pb-6 pt-7">
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>{active.options.title}</DialogTitle>
|
||||
|
||||
{active.options.description && (
|
||||
<DialogDescription asChild>
|
||||
<div>{active.options.description}</div>
|
||||
</DialogDescription>)}
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter>
|
||||
{active.kind === 'confirm' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => closeActive (false)}>
|
||||
{active.options.cancelText ?? '取消'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={(active.options.variant === 'danger')
|
||||
? 'destructive'
|
||||
: 'default'}
|
||||
onClick={() => closeActive (true)}>
|
||||
{active.options.confirmText ?? '確定'}
|
||||
</Button>
|
||||
</>)}
|
||||
|
||||
{active.kind === 'alert' && (
|
||||
<Button onClick={() => closeActive ()}>
|
||||
{active.options.okText ?? '確定'}
|
||||
</Button>)}
|
||||
|
||||
{active.kind === 'choice' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => closeActive (null)}>
|
||||
{active.options.cancelText ?? '取消'}
|
||||
</Button>
|
||||
|
||||
{active.options.choices.map (choice => (
|
||||
<Button
|
||||
key={choice.value}
|
||||
variant={(choice.variant === 'danger')
|
||||
? 'destructive'
|
||||
: 'default'}
|
||||
onClick={() => closeActive (choice.value)}>
|
||||
{choice.label}
|
||||
</Button>))}
|
||||
</>)}
|
||||
</DialogFooter>
|
||||
</DialogContent>)}
|
||||
</Dialog>
|
||||
</DialogueContext.Provider>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
|
||||
export const useDialogue = () => {
|
||||
const dialogue = useContext (DialogueContext)
|
||||
|
||||
if (!(dialogue))
|
||||
throw new Error ('useDialogue must be used inside DialogueProvider')
|
||||
|
||||
return dialogue
|
||||
}
|
||||
@@ -4,34 +4,47 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
const buttonVariants = cva (
|
||||
[
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap',
|
||||
'rounded-md text-sm font-medium transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
'[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
].join (' '),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default:
|
||||
'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-300',
|
||||
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
'bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600',
|
||||
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
'border border-slate-300 bg-white text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800',
|
||||
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700',
|
||||
|
||||
ghost:
|
||||
'text-slate-900 hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-800',
|
||||
|
||||
link:
|
||||
'text-blue-700 underline-offset-4 hover:underline dark:text-blue-300',
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
|
||||
@@ -37,25 +37,29 @@ 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 left-4 top-4 rounded-full p-1',
|
||||
'text-slate-500 transition-colors',
|
||||
'hover:bg-slate-200 hover:text-slate-900',
|
||||
'dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-50',
|
||||
'focus:outline-none focus:ring-2 focus:ring-slate-400')}>
|
||||
<X className="h-4 w-4"/>
|
||||
<span className="sr-only">閉ぢる</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog,
|
||||
DialogContent,
|
||||
DialogTitle } from '@/components/ui/dialog'
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { apiPost } from '@/lib/api'
|
||||
@@ -16,10 +19,16 @@ type Props = { visible: boolean
|
||||
|
||||
|
||||
export default ({ visible, onVisibleChange, setUser }: Props) => {
|
||||
const dialogue = useDialogue ()
|
||||
|
||||
const [inputCode, setInputCode] = useState ('')
|
||||
|
||||
const handleTransfer = async () => {
|
||||
if (!(confirm ('引継ぎを行ってもよろしいですか?\n現在のアカウントからはログアウトされます.')))
|
||||
if (!(await dialogue.confirm ({
|
||||
title: '引継ぎを行ってもよろしいですか?',
|
||||
description: '現在のアカウントからはログアウトされます.',
|
||||
confirmText: '引継ぐ',
|
||||
variant: 'danger' })))
|
||||
return
|
||||
|
||||
try
|
||||
@@ -44,14 +53,18 @@ export default ({ visible, onVisibleChange, setUser }: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={onVisibleChange}>
|
||||
<DialogContent>
|
||||
<DialogTitle>ほかのブラウザから引継ぐ</DialogTitle>
|
||||
<div className="flex gap-2">
|
||||
<Input placeholder="引継ぎコードを入力"
|
||||
value={inputCode}
|
||||
onChange={ev => setInputCode (ev.target.value)}/>
|
||||
<Button onClick={handleTransfer}>引継ぐ</Button>
|
||||
</div>
|
||||
<DialogContent className="px-6 pp-6 pt-7">
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>ほかのブラウザから引継ぐ</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div className="flex gap-2">
|
||||
<Input placeholder="引継ぎコードを入力"
|
||||
value={inputCode}
|
||||
onChange={ev => setInputCode (ev.target.value)}/>
|
||||
<Button onClick={handleTransfer}>引継ぐ</Button>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle } from '@/components/ui/dialog'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { apiPost } from '@/lib/api'
|
||||
@@ -14,11 +18,20 @@ type Props = { visible: boolean
|
||||
|
||||
|
||||
export default ({ visible, onVisibleChange, user, setUser }: Props) => {
|
||||
const dialogue = useDialogue ()
|
||||
|
||||
const handleChange = async () => {
|
||||
if (!(user))
|
||||
return
|
||||
|
||||
if (!(confirm ('引継ぎコードを再発行しますか?\n再発行するとほかのブラウザからはログアウトされます.')))
|
||||
if (!(await dialogue.confirm ({
|
||||
title: '引継ぎコードを再発行しますか?',
|
||||
description: (
|
||||
<div>
|
||||
<p>再発行するとほかのブラウザからはログアウトされます.</p>
|
||||
</div>),
|
||||
confirmText: '再発行',
|
||||
variant: 'danger' })))
|
||||
return
|
||||
|
||||
const data = await apiPost<{ code: string }> ('/users/code/renew', { },
|
||||
@@ -33,21 +46,26 @@ export default ({ visible, onVisibleChange, user, setUser }: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={onVisibleChange}>
|
||||
<DialogContent>
|
||||
<DialogTitle>引継ぎコード</DialogTitle>
|
||||
<div>
|
||||
<p>あなたの引継ぎコードはこちらです:</p>
|
||||
<div className="m-2">{user?.inheritanceCode}</div>
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
このコードはほかの人には教えないでください!
|
||||
</p>
|
||||
<div className="my-4">
|
||||
<Button onClick={handleChange}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded disabled:bg-gray-400">
|
||||
引継ぎコード再発行
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogContent className="px-6 pb-6 pt-7">
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>引継ぎコード</DialogTitle>
|
||||
|
||||
<DialogDescription asChild>
|
||||
<div>
|
||||
<p>あなたの引継ぎコードはこちらです:</p>
|
||||
<div className="m-2">{user?.inheritanceCode}</div>
|
||||
<p className="mt-1 text-sm text-destructive">
|
||||
このコードはほかの人には教えないでください!
|
||||
</p>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleChange} variant="destructive">
|
||||
引継ぎコード再発行
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user