| @@ -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> | |||
| @@ -36,7 +36,7 @@ type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | ' | |||
| setTags: (tags: string) => void } | |||
| export default (({ tags, setTags, onBlur, ...rest }: Props) => { | |||
| export default (({ tags, setTags, ...rest }: Props) => { | |||
| const ref = useRef<HTMLTextAreaElement> (null) | |||
| const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 }) | |||
| @@ -85,10 +85,9 @@ export default (({ tags, setTags, onBlur, ...rest }: Props) => { | |||
| await recompute (pos) | |||
| }} | |||
| onFocus={() => setFocused (true)} | |||
| onBlur={ev => { | |||
| onBlur={() => { | |||
| setFocused (false) | |||
| setSuggestionsVsbl (false) | |||
| onBlur?.(ev) | |||
| }}/> | |||
| {focused && ( | |||
| <TagSearchBox | |||
| @@ -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>) | |||
| } | |||
| @@ -8,6 +8,7 @@ import TagLink from '@/components/TagLink' | |||
| import PrefetchLink from '@/components/PrefetchLink' | |||
| import PageTitle from '@/components/common/PageTitle' | |||
| import Pagination from '@/components/common/Pagination' | |||
| import { useDialogue } from '@/components/dialogues/DialogueProvider' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { toast } from '@/components/ui/use-toast' | |||
| import { SITE_TITLE } from '@/config' | |||
| @@ -35,6 +36,8 @@ const renderDiff = (diff: { current: string | null; prev: string | null }) => ( | |||
| export default (() => { | |||
| const dialogue = useDialogue () | |||
| const location = useLocation () | |||
| const query = new URLSearchParams (location.search) | |||
| const id = query.get ('id') | |||
| @@ -66,8 +69,11 @@ export default (() => { | |||
| const handleRevert = async (e: MouseEvent<HTMLAnchorElement>, change: PostVersion) => { | |||
| e.preventDefault () | |||
| if (!(confirm (`『${ change.title.current || change.url.current }』を版 ${ | |||
| change.versionNo } に差戻します.\nよろしいですか?`))) | |||
| if (!(await dialogue.confirm ({ | |||
| title: '差戻の確認', | |||
| description: `『${ change.title.current || change.url.current }』を版 ${ | |||
| change.versionNo } に差戻します.\nよろしいですか?`, | |||
| confirmText: '差戻' }))) | |||
| return | |||
| try | |||