このコミットが含まれているのは:
2026-06-25 08:11:41 +09:00
コミット c10ba7a698
36個のファイルの変更886行の追加1132行の削除
+13
ファイルの表示
@@ -268,7 +268,20 @@ const value =
- Put two blank lines before and after top-level `const` function - Put two blank lines before and after top-level `const` function
declarations, unless imports, exports, or file boundaries make that awkward. declarations, unless imports, exports, or file boundaries make that awkward.
- In TSX, indent with 4-space logical indentation. - In TSX, indent with 4-space logical indentation.
- In TypeScript and TSX, convert every leading run of 8 spaces to a tab
character.
- A leading tab is exactly equivalent to 8 leading spaces. - A leading tab is exactly equivalent to 8 leading spaces.
- Never place a closing parenthesis at the beginning of a line.
- Never place a closing square bracket at the beginning of a line.
- For object literals and other associative-array-style braces, do not place
the closing brace at the beginning of a line. Function, lambda, callback, and
block closing braces are exempt and should stay on their own line when that
fits the local style.
- When writing braces on a single line in TypeScript or TSX JavaScript
context, put exactly one space inside the braces, as in `{ value }` or
`{ key: value }`.
- Do not add inner spaces to React/JSX expression braces, as in
`prop={value}`, `{children}`, or `<Component>{{...props}}</Component>`.
- Keep a tag's closing marker on the same line as the final prop when the tag - Keep a tag's closing marker on the same line as the final prop when the tag
spans multiple lines. spans multiple lines.
- Do not put `/>` or `>` on its own line unless the existing surrounding code - Do not put `/>` or `>` on its own line unless the existing surrounding code
+7 -30
ファイルの表示
@@ -2,7 +2,6 @@ import { Fragment, useEffect, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink' import TagLink from '@/components/TagLink'
import SidebarComponent from '@/components/layout/SidebarComponent' import SidebarComponent from '@/components/layout/SidebarComponent'
import { materialsKeys } from '@/lib/queryKeys' import { materialsKeys } from '@/lib/queryKeys'
@@ -17,15 +16,13 @@ const FILTERS: MaterialFilter[] = ['present', 'missing', 'any']
const FILTER_LABELS: Record<MaterialFilter, string> = { const FILTER_LABELS: Record<MaterialFilter, string> = {
present: '素材あり', present: '素材あり',
missing: '素材なし', missing: '素材なし',
any: 'すべて', any: 'すべて'}
}
const setChildrenById = ( const setChildrenById = (
tags: MaterialSidebarTag[], tags: MaterialSidebarTag[],
targetId: number, targetId: number,
children: MaterialSidebarTag[], children: MaterialSidebarTag[]): MaterialSidebarTag[] => (
): MaterialSidebarTag[] => (
tags.map (tag => { tags.map (tag => {
if (tag.id === targetId) if (tag.id === targetId)
return { ...tag, children } return { ...tag, children }
@@ -39,8 +36,7 @@ const setChildrenById = (
const materialPath = ( const materialPath = (
tagName: string, tagName: string,
materialFilter: MaterialFilter, materialFilter: MaterialFilter): string => `/materials?q=${ encodeURIComponent (tagName) }&material_filter=${ materialFilter }`
): string => `/materials?q=${ encodeURIComponent (tagName) }&material_filter=${ materialFilter }`
const sidebarTagToTag = (tag: MaterialSidebarTag): Tag => ({ const sidebarTagToTag = (tag: MaterialSidebarTag): Tag => ({
@@ -63,8 +59,7 @@ const updateMaterialFilterQuery = (
pathname: string, pathname: string,
locationSearch: string, locationSearch: string,
navigate: ReturnType<typeof useNavigate>, navigate: ReturnType<typeof useNavigate>,
materialFilter: MaterialFilter, materialFilter: MaterialFilter) => {
) => {
const qs = new URLSearchParams (locationSearch) const qs = new URLSearchParams (locationSearch)
qs.set ('material_filter', materialFilter) qs.set ('material_filter', materialFilter)
navigate (`${ pathname }${ qs.toString () ? `?${ qs.toString () }` : '' }`) navigate (`${ pathname }${ qs.toString () ? `?${ qs.toString () }` : '' }`)
@@ -104,8 +99,7 @@ const MaterialTreeNode: FC<{
const { data } = useQuery ({ const { data } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }), queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }), queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
enabled: open && tag.hasChildren && tag.children.length === 0, enabled: open && tag.hasChildren && tag.children.length === 0})
})
useEffect (() => { useEffect (() => {
if (open && data && tag.children.length === 0) if (open && data && tag.children.length === 0)
@@ -164,8 +158,7 @@ const MobileMaterialTreeNode: FC<{
const { data } = useQuery ({ const { data } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }), queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }), queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
enabled: open && tag.hasChildren && tag.children.length === 0, enabled: open && tag.hasChildren && tag.children.length === 0})
})
useEffect (() => { useEffect (() => {
if (open && data && tag.children.length === 0) if (open && data && tag.children.length === 0)
@@ -233,8 +226,7 @@ const MaterialSidebar: FC = () => {
const { data: rootTags = [], isLoading, isError } = useQuery ({ const { data: rootTags = [], isLoading, isError } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: null, materialFilter }), queryKey: materialsKeys.tree ({ parentId: null, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter }), queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter })})
})
useEffect (() => { useEffect (() => {
setDesktopTags (rootTags) setDesktopTags (rootTags)
@@ -279,14 +271,6 @@ const MaterialSidebar: FC = () => {
<> <>
<div className="border-b bg-stone-50 p-3 dark:border-stone-700 dark:bg-stone-950 <div className="border-b bg-stone-50 p-3 dark:border-stone-700 dark:bg-stone-950
dark:text-stone-100 md:hidden"> dark:text-stone-100 md:hidden">
<div className="mb-3 flex items-center justify-between gap-3">
<span className="text-sm font-medium text-stone-700 dark:text-stone-200"></span>
<PrefetchLink
to={`/materials?unclassified=1&material_filter=${ materialFilter }`}
className="text-sm text-sky-700 underline underline-offset-2 dark:text-sky-300">
</PrefetchLink>
</div>
<MaterialFilterButtons <MaterialFilterButtons
materialFilter={materialFilter} materialFilter={materialFilter}
onChange={handleFilterChange}/> onChange={handleFilterChange}/>
@@ -310,13 +294,6 @@ const MaterialSidebar: FC = () => {
<MaterialFilterButtons <MaterialFilterButtons
materialFilter={materialFilter} materialFilter={materialFilter}
onChange={handleFilterChange}/> onChange={handleFilterChange}/>
<div>
<PrefetchLink
to={`/materials?unclassified=1&material_filter=${ materialFilter }`}
className="text-sm text-sky-700 underline underline-offset-2 dark:text-sky-300">
</PrefetchLink>
</div>
{isLoading && ( {isLoading && (
<p className="text-sm text-neutral-500 dark:text-stone-400"></p>)} <p className="text-sm text-neutral-500 dark:text-stone-400"></p>)}
{isError && ( {isError && (
+1 -2
ファイルの表示
@@ -107,8 +107,7 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
loadCompleteTimerRef.current = setTimeout (() => { loadCompleteTimerRef.current = setTimeout (() => {
onError?.({ onError?.({
eventName: 'loadCompleteTimeout', eventName: 'loadCompleteTimeout',
reason: 'niconico video length was not reported by embed', reason: 'niconico video length was not reported by embed'})
})
}, LOAD_COMPLETE_TIMEOUT_MS) }, LOAD_COMPLETE_TIMEOUT_MS)
}, [clearLoadCompleteTimer, onError]) }, [clearLoadCompleteTimer, onError])
+1 -2
ファイルの表示
@@ -19,8 +19,7 @@ const PostOriginalCreatedTimeField: FC<Props> = (
setOriginalCreatedFrom, setOriginalCreatedFrom,
originalCreatedBefore, originalCreatedBefore,
setOriginalCreatedBefore, setOriginalCreatedBefore,
errors }: Props, errors }: Props) => (
) => (
<FormField label="オリジナルの作成日時" messages={errors}> <FormField label="オリジナルの作成日時" messages={errors}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<> <>
+5 -10
ファイルの表示
@@ -37,8 +37,7 @@ const renderTagTree = (
path: string, path: string,
suppressClickRef: MutableRefObject<boolean>, suppressClickRef: MutableRefObject<boolean>,
parentTagId?: number, parentTagId?: number,
sp?: boolean, sp?: boolean): ReactNode[] => {
): ReactNode[] => {
const key = `${ path }-${ tag.id }` const key = `${ path }-${ tag.id }`
const self = ( const self = (
@@ -64,8 +63,7 @@ const renderTagTree = (
const isDescendant = ( const isDescendant = (
root: Tag, root: Tag,
targetId: number, targetId: number): boolean => {
): boolean => {
if (!(root.children)) if (!(root.children))
return false return false
@@ -83,8 +81,7 @@ const isDescendant = (
const findTag = ( const findTag = (
byCat: TagByCategory, byCat: TagByCategory,
id: number, id: number): Tag | undefined => {
): Tag | undefined => {
const walk = (nodes: Tag[]): Tag | undefined => { const walk = (nodes: Tag[]): Tag | undefined => {
for (const t of nodes) for (const t of nodes)
{ {
@@ -130,8 +127,7 @@ const buildTagByCategory = (post: Post): TagByCategory => {
const changeCategory = async ( const changeCategory = async (
tagId: number, tagId: number,
category: Category, category: Category): Promise<void> => {
): Promise<void> => {
await apiPatch (`/tags/${ tagId }`, { category }) await apiPatch (`/tags/${ tagId }`, { category })
} }
@@ -294,8 +290,7 @@ const TagDetailSidebar: FC<Props> = ({ post, sp }) => {
addEventListener ('click', e => { addEventListener ('click', e => {
e.preventDefault () e.preventDefault ()
e.stopPropagation () e.stopPropagation ()
suppressClickRef.current = false suppressClickRef.current = false}, { capture: true, once: true })
}, { capture: true, once: true })
}} }}
onDragCancel={() => { onDragCancel={() => {
setActiveTagId (null) setActiveTagId (null)
+1 -2
ファイルの表示
@@ -16,8 +16,7 @@ const range = (start: number, end: number): number[] =>
const getPages = ( const getPages = (
page: number, page: number,
total: number, total: number,
siblingCount: number, siblingCount: number): (number | '…')[] => {
): (number | '…')[] => {
if (total <= 1) if (total <= 1)
return [1] return [1]
+1 -2
ファイルの表示
@@ -103,8 +103,7 @@ const DialogueProvider: FC<Props> = ({ children }) => {
choice: options => new Promise (resolve => { choice: options => new Promise (resolve => {
push ({ kind: 'choice', push ({ kind: 'choice',
options: options as ChoiceOptions<string>, options: options as ChoiceOptions<string>,
resolve: resolve as (value: string | null) => void }) resolve: resolve as (value: string | null) => void })}) }), [push])
}) }), [push])
const active = queue[0] const active = queue[0]
+6 -14
ファイルの表示
@@ -10,8 +10,7 @@ const buttonVariants = cva (
'rounded-md text-sm font-medium transition-colors', 'rounded-md text-sm font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400',
'disabled:pointer-events-none disabled:opacity-50', 'disabled:pointer-events-none disabled:opacity-50',
'[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', '[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'].join (' '),
].join (' '),
{ {
variants: { variants: {
variant: { variant: {
@@ -31,20 +30,15 @@ const buttonVariants = cva (
'text-slate-900 hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-800', 'text-slate-900 hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-800',
link: link:
'text-blue-700 underline-offset-4 hover:underline dark:text-blue-300', 'text-blue-700 underline-offset-4 hover:underline dark:text-blue-300'},
},
size: { size: {
default: 'h-10 px-4 py-2', default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3', sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8', lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10', icon: 'h-10 w-10'}},
},
},
defaultVariants: { defaultVariants: {
variant: 'default', variant: 'default',
size: 'default', size: 'default'}})
},
})
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -60,10 +54,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />)
) })
}
)
Button.displayName = "Button" Button.displayName = "Button"
export { Button, buttonVariants } export { Button, buttonVariants }
+10 -20
ファイルの表示
@@ -22,11 +22,9 @@ const DialogOverlay = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className)}
)}
{...props} {...props}
/> />))
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
@@ -62,8 +60,7 @@ const DialogContent = React.forwardRef<
<span className="sr-only"></span> <span className="sr-only"></span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>))
))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ const DialogHeader = ({
@@ -73,11 +70,9 @@ const DialogHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", "flex flex-col space-y-1.5 text-center sm:text-left",
className className)}
)}
{...props} {...props}
/> />)
)
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ const DialogFooter = ({
@@ -87,11 +82,9 @@ const DialogFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className)}
)}
{...props} {...props}
/> />)
)
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
@@ -102,11 +95,9 @@ const DialogTitle = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", "text-lg font-semibold leading-none tracking-tight",
className className)}
)}
{...props} {...props}
/> />))
))
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
@@ -117,8 +108,7 @@ const DialogDescription = React.forwardRef<
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />))
))
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName
export { export {
+3 -6
ファイルの表示
@@ -9,14 +9,11 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className
)}
)} )}
ref={ref} ref={ref}
/> {...props}
) />
}
)
) )
} }
) )
+3 -6
ファイルの表示
@@ -12,18 +12,15 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className className
)}
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" className={cn(
)}
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
</SwitchPrimitives.Root> )}
))
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>
)) ))
+10 -24
ファイルの表示
@@ -17,11 +17,9 @@ const ToastViewport = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className className
)}
)} )}
/> {...props}
))
/> />
)) ))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
@@ -34,11 +32,7 @@ const toastVariants = cva(
default: "border bg-background text-foreground", default: "border bg-background text-foreground",
destructive: destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground", "destructive group border-destructive bg-destructive text-destructive-foreground",
defaultVariants: { },
variant: "default",
},
}
)
}, },
defaultVariants: { variant: "default" } } defaultVariants: { variant: "default" } }
) )
@@ -50,9 +44,7 @@ const Toast = React.forwardRef<
>(({ className, variant, ...props }, ref) => { >(({ className, variant, ...props }, ref) => {
return ( return (
<ToastPrimitives.Root <ToastPrimitives.Root
/> ref={ref}
)
})
className={cn(toastVariants({ variant }), className)} className={cn(toastVariants({ variant }), className)}
{...props} {...props}
/> />
@@ -63,11 +55,9 @@ const ToastAction = React.forwardRef<
const ToastAction = React.forwardRef< const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>, React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
className >(({ className, ...props }, ref) => (
)}
<ToastPrimitives.Action <ToastPrimitives.Action
/> ref={ref}
))
className={cn( className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className className
@@ -78,14 +68,12 @@ const ToastClose = React.forwardRef<
ToastAction.displayName = ToastPrimitives.Action.displayName ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef< const ToastClose = React.forwardRef<
className React.ElementRef<typeof ToastPrimitives.Close>,
)}
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={ref}
</ToastPrimitives.Close> className={cn(
))
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className className
)} )}
@@ -96,8 +84,7 @@ const ToastTitle = React.forwardRef<
</ToastPrimitives.Close> </ToastPrimitives.Close>
)) ))
ToastClose.displayName = ToastPrimitives.Close.displayName ToastClose.displayName = ToastPrimitives.Close.displayName
/>
))
const ToastTitle = React.forwardRef< const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>, React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
@@ -108,8 +95,7 @@ const ToastDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
/> ToastTitle.displayName = ToastPrimitives.Title.displayName
))
const ToastDescription = React.forwardRef< const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>, React.ElementRef<typeof ToastPrimitives.Description>,
+1 -2
ファイルの表示
@@ -26,6 +26,5 @@ export const Toaster = () => {
<ToastClose /> <ToastClose />
</Toast>))} </Toast>))}
<ToastViewport /> <ToastViewport />
</ToastProvider> </ToastProvider>
)
) )
+11 -25
ファイルの表示
@@ -58,8 +58,7 @@ const addToRemoveQueue = (toastId: string) => {
toastTimeouts.delete(toastId) toastTimeouts.delete(toastId)
dispatch({ dispatch({
type: "REMOVE_TOAST", type: "REMOVE_TOAST",
toastId: toastId, toastId: toastId})
})
}, TOAST_REMOVE_DELAY) }, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout) toastTimeouts.set(toastId, timeout)
@@ -70,16 +69,13 @@ export const reducer = (state: State, action: Action): State => {
case "ADD_TOAST": case "ADD_TOAST":
return { return {
...state, ...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)}
}
case "UPDATE_TOAST": case "UPDATE_TOAST":
return { return {
...state, ...state,
toasts: state.toasts.map((t) => toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t t.id === action.toast.id ? { ...t, ...action.toast } : t)}
),
}
case "DISMISS_TOAST": { case "DISMISS_TOAST": {
const { toastId } = action const { toastId } = action
@@ -100,23 +96,18 @@ export const reducer = (state: State, action: Action): State => {
t.id === toastId || toastId === undefined t.id === toastId || toastId === undefined
? { ? {
...t, ...t,
open: false, open: false}
} : t)}
: t
),
}
} }
case "REMOVE_TOAST": case "REMOVE_TOAST":
if (action.toastId === undefined) { if (action.toastId === undefined) {
return { return {
...state, ...state,
toasts: [], toasts: []}
}
} }
return { return {
...state, ...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId), toasts: state.toasts.filter((t) => t.id !== action.toastId)}
}
} }
} }
@@ -139,8 +130,7 @@ function toast({ ...props }: Toast) {
const update = (props: ToasterToast) => const update = (props: ToasterToast) =>
dispatch({ dispatch({
type: "UPDATE_TOAST", type: "UPDATE_TOAST",
toast: { ...props, id }, toast: { ...props, id }})
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({ dispatch({
@@ -151,15 +141,12 @@ function toast({ ...props }: Toast) {
open: true, open: true,
onOpenChange: (open) => { onOpenChange: (open) => {
if (!open) dismiss() if (!open) dismiss()
}, }}})
},
})
return { return {
id: id, id: id,
dismiss, dismiss,
update, update}
}
} }
function useToast() { function useToast() {
@@ -178,8 +165,7 @@ function useToast() {
return { return {
...state, ...state,
toast, toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId })}
}
} }
export { useToast, toast } export { useToast, toast }
+1 -2
ファイルの表示
@@ -3,8 +3,7 @@ const ENV: string = 'development'
const config = { const config = {
API_BASE_URL: ENV === 'production' ? 'https://hub.nizika.monster/api' : 'http://localhost:3002', API_BASE_URL: ENV === 'production' ? 'https://hub.nizika.monster/api' : 'http://localhost:3002',
SITE_TITLE: 'ぼざクリ タグ広場' SITE_TITLE: 'ぼざクリ タグ広場'}
}
export const API_BASE_URL = config.API_BASE_URL export const API_BASE_URL = config.API_BASE_URL
export const SITE_TITLE = config.SITE_TITLE export const SITE_TITLE = config.SITE_TITLE
+5 -10
ファイルの表示
@@ -10,8 +10,7 @@ export const CATEGORIES = [
'general', 'general',
'material', 'material',
'meta', 'meta',
'nico', 'nico'] as const
] as const
export const CATEGORY_NAMES: Record<Category, string> = { export const CATEGORY_NAMES: Record<Category, string> = {
deerjikist: 'ニジラー', deerjikist: 'ニジラー',
@@ -20,16 +19,14 @@ export const CATEGORY_NAMES: Record<Category, string> = {
general: '一般', general: '一般',
material: '素材', material: '素材',
meta: 'メタタグ', meta: 'メタタグ',
nico: 'ニコニコタグ', nico: 'ニコニコタグ'} as const
} as const
export const FETCH_POSTS_ORDER_FIELDS = [ export const FETCH_POSTS_ORDER_FIELDS = [
'title', 'title',
'url', 'url',
'original_created_at', 'original_created_at',
'created_at', 'created_at',
'updated_at', 'updated_at'] as const
] as const
export const PLATFORMS = ['nico', 'youtube'] as const export const PLATFORMS = ['nico', 'youtube'] as const
@@ -43,13 +40,11 @@ export const TAG_COLOUR = {
general: 'cyan', general: 'cyan',
material: 'orange', material: 'orange',
meta: 'yellow', meta: 'yellow',
nico: 'gray', nico: 'gray'} as const satisfies Record<Category, string>
} as const satisfies Record<Category, string>
export const USER_ROLES = ['admin', 'member', 'guest'] as const export const USER_ROLES = ['admin', 'member', 'guest'] as const
export const ViewFlagBehavior = { export const ViewFlagBehavior = {
OnShowedDetail: 1, OnShowedDetail: 1,
OnClickedLink: 2, OnClickedLink: 2,
NotAuto: 3, NotAuto: 3} as const
} as const
+6 -12
ファイルの表示
@@ -23,8 +23,7 @@ const apiP = async <T> (
method: 'post' | 'put' | 'patch', method: 'post' | 'put' | 'patch',
path: string, path: string,
body?: unknown, body?: unknown,
opt?: Opt, opt?: Opt): Promise<T> => {
): Promise<T> => {
const res = await client[method] (path, body ?? { }, withUserCode (opt)) const res = await client[method] (path, body ?? { }, withUserCode (opt))
if (opt?.responseType === 'blob') if (opt?.responseType === 'blob')
return res.data as T return res.data as T
@@ -34,8 +33,7 @@ const apiP = async <T> (
export const apiGet = async <T> ( export const apiGet = async <T> (
path: string, path: string,
opt?: Opt, opt?: Opt): Promise<T> => {
): Promise<T> => {
const res = await client.get (path, withUserCode (opt)) const res = await client.get (path, withUserCode (opt))
if (opt?.responseType === 'blob') if (opt?.responseType === 'blob')
return res.data as T return res.data as T
@@ -46,28 +44,24 @@ export const apiGet = async <T> (
export const apiPost = async <T> ( export const apiPost = async <T> (
path: string, path: string,
body?: unknown, body?: unknown,
opt?: Opt, opt?: Opt): Promise<T> => apiP ('post', path, body, opt)
): Promise<T> => apiP ('post', path, body, opt)
export const apiPut = async <T> ( export const apiPut = async <T> (
path: string, path: string,
body?: unknown, body?: unknown,
opt?: Opt, opt?: Opt): Promise<T> => apiP ('put', path, body, opt)
): Promise<T> => apiP ('put', path, body, opt)
export const apiPatch = async <T> ( export const apiPatch = async <T> (
path: string, path: string,
body?: unknown, body?: unknown,
opt?: Opt, opt?: Opt): Promise<T> => apiP ('patch', path, body, opt)
): Promise<T> => apiP ('patch', path, body, opt)
export const apiDelete = async <T = void> ( export const apiDelete = async <T = void> (
path: string, path: string,
opt?: Opt, opt?: Opt): Promise<T> => {
): Promise<T> => {
const res = await client.delete (path, withUserCode (opt)) const res = await client.delete (path, withUserCode (opt))
if (res.data == null || res.data === '') if (res.data == null || res.data === '')
return undefined as T return undefined as T
+14 -28
ファイルの表示
@@ -104,8 +104,7 @@ export type BuildGekanatorQuestionsOptions = {
export const normalizeTitleLengthCondition = ( export const normalizeTitleLengthCondition = (
condition: GekanatorQuestionCondition, condition: GekanatorQuestionCondition): GekanatorQuestionCondition => {
): GekanatorQuestionCondition => {
switch (condition.type) switch (condition.type)
{ {
case 'title-length-greater-than': case 'title-length-greater-than':
@@ -119,8 +118,7 @@ export const normalizeTitleLengthCondition = (
export const titleLengthMinimumForCondition = ( export const titleLengthMinimumForCondition = (
condition: GekanatorQuestionCondition, condition: GekanatorQuestionCondition): number | null => {
): number | null => {
switch (condition.type) switch (condition.type)
{ {
case 'title-length-at-least': case 'title-length-at-least':
@@ -134,8 +132,7 @@ export const titleLengthMinimumForCondition = (
export const questionIdForCondition = ( export const questionIdForCondition = (
condition: NonPostSimilarityCondition, condition: NonPostSimilarityCondition): string => {
): string => {
switch (condition.type) switch (condition.type)
{ {
case 'tag': case 'tag':
@@ -161,8 +158,7 @@ export const questionIdForCondition = (
const directExampleAnswerFor = ( const directExampleAnswerFor = (
question: StoredGekanatorQuestion, question: StoredGekanatorQuestion,
post: Post, post: Post): GekanatorAnswerValue | null => {
): GekanatorAnswerValue | null => {
if (question.kind !== 'post_similarity' && question.kind !== 'tag') if (question.kind !== 'post_similarity' && question.kind !== 'tag')
return null return null
@@ -178,15 +174,13 @@ const directExampleAnswerFor = (
export const isLearnedSemanticQuestion = ( export const isLearnedSemanticQuestion = (
question: StoredGekanatorQuestion | GekanatorQuestion, question: StoredGekanatorQuestion | GekanatorQuestion): boolean =>
): boolean =>
question.kind === 'post_similarity' question.kind === 'post_similarity'
&& question.source === 'user_suggested' && question.source === 'user_suggested'
export const learnedSemanticSideForAnswer = ( export const learnedSemanticSideForAnswer = (
answer: GekanatorAnswerValue | null, answer: GekanatorAnswerValue | null): LearnedSemanticSide => {
): LearnedSemanticSide => {
if (answer === 'yes' || answer === 'partial') if (answer === 'yes' || answer === 'partial')
return 'positive' return 'positive'
@@ -314,8 +308,7 @@ const questionableTag = (post: Post, key: string): boolean => {
const questionMatches = ( const questionMatches = (
post: Post, post: Post,
question: StoredGekanatorQuestion, question: StoredGekanatorQuestion): boolean => {
): boolean => {
const directAnswer = directExampleAnswerFor (question, post) const directAnswer = directExampleAnswerFor (question, post)
if (directAnswer) if (directAnswer)
return question.kind === 'post_similarity' return question.kind === 'post_similarity'
@@ -350,8 +343,7 @@ const questionMatches = (
export const expectedAnswerForQuestion = ( export const expectedAnswerForQuestion = (
question: StoredGekanatorQuestion | GekanatorQuestion | undefined, question: StoredGekanatorQuestion | GekanatorQuestion | undefined,
post: Post | null, post: Post | null): GekanatorAnswerValue | null => {
): GekanatorAnswerValue | null => {
if (!(question) || !(post)) if (!(question) || !(post))
return null return null
@@ -382,14 +374,12 @@ export const expectedAnswerForQuestion = (
export const learnedSemanticSideForPost = ( export const learnedSemanticSideForPost = (
question: StoredGekanatorQuestion | GekanatorQuestion | undefined, question: StoredGekanatorQuestion | GekanatorQuestion | undefined,
post: Post | null, post: Post | null): LearnedSemanticSide =>
): LearnedSemanticSide =>
learnedSemanticSideForAnswer (expectedAnswerForQuestion (question, post)) learnedSemanticSideForAnswer (expectedAnswerForQuestion (question, post))
export const restoreGekanatorQuestion = ( export const restoreGekanatorQuestion = (
question: StoredGekanatorQuestion, question: StoredGekanatorQuestion): GekanatorQuestion => {
): GekanatorQuestion => {
const normalizedCondition = normalizeTitleLengthCondition (question.condition) const normalizedCondition = normalizeTitleLengthCondition (question.condition)
const normalizedQuestion = { const normalizedQuestion = {
...question, ...question,
@@ -408,8 +398,7 @@ export const restoreGekanatorQuestion = (
export const storeGekanatorQuestion = ( export const storeGekanatorQuestion = (
question: GekanatorQuestion, question: GekanatorQuestion): StoredGekanatorQuestion => ({
): StoredGekanatorQuestion => ({
id: question.condition.type === 'title-length-greater-than' id: question.condition.type === 'title-length-greater-than'
? `title:length-at-least:${ question.condition.length + 1 }` ? `title:length-at-least:${ question.condition.length + 1 }`
: question.id, : question.id,
@@ -436,8 +425,7 @@ export const fetchGekanatorQuestions = async (): Promise<StoredGekanatorQuestion
export const fetchGekanatorExtraQuestions = async ( export const fetchGekanatorExtraQuestions = async (
gameId: number, gameId: number,
nonce?: string, nonce?: string): Promise<GekanatorExtraQuestion[]> => {
): Promise<GekanatorExtraQuestion[]> => {
const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> ( const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> (
`/gekanator/games/${ gameId }/extra_questions`, `/gekanator/games/${ gameId }/extra_questions`,
{ params: nonce ? { nonce } : undefined }) { params: nonce ? { nonce } : undefined })
@@ -447,8 +435,7 @@ export const fetchGekanatorExtraQuestions = async (
export const buildGekanatorQuestions = ( export const buildGekanatorQuestions = (
posts: Post[], posts: Post[],
options: BuildGekanatorQuestionsOptions = { }, options: BuildGekanatorQuestionsOptions = { }): GekanatorQuestion[] => {
): GekanatorQuestion[] => {
const { const {
includeTitleContains = true, includeTitleContains = true,
tagQuestionCap = 192, tagQuestionCap = 192,
@@ -490,8 +477,7 @@ export const buildGekanatorQuestions = (
const usefulEntries = <T extends string | number> ( const usefulEntries = <T extends string | number> (
counts: Map<T, number>, counts: Map<T, number>,
cap: number, cap: number) =>
) =>
[...counts.entries ()] [...counts.entries ()]
.filter (([, count]) => count > 0 && count < posts.length) .filter (([, count]) => count > 0 && count < posts.length)
.sort ((a, b) => Math.abs (posts.length / 2 - a[1]) .sort ((a, b) => Math.abs (posts.length / 2 - a[1])
+4 -8
ファイルの表示
@@ -32,8 +32,7 @@ export const candidatePostsFor = (
answers: GekanatorAnswerLog[] answers: GekanatorAnswerLog[]
softenedQuestionIds: Set<string> softenedQuestionIds: Set<string>
rejectedPostIds: Set<number> rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, RecoveredCandidateState> }, recoveredCandidatePosts: Map<number, RecoveredCandidateState> }): Post[] => {
): Post[] => {
const questionById = new Map (questions.map (question => [question.id, question])) const questionById = new Map (questions.map (question => [question.id, question]))
return posts.filter (post => { return posts.filter (post => {
@@ -76,8 +75,7 @@ export const candidatePostsFor = (
export const hardFilteredPostsForAnswer = ( export const hardFilteredPostsForAnswer = (
{ posts, question, answer }: { posts: Post[] { posts, question, answer }: { posts: Post[]
question: GekanatorQuestion question: GekanatorQuestion
answer: GekanatorAnswerValue }, answer: GekanatorAnswerValue }): Post[] => {
): Post[] => {
if (!(questionSupportsAnswerBasedHardFiltering (question))) if (!(questionSupportsAnswerBasedHardFiltering (question)))
return posts return posts
@@ -98,8 +96,7 @@ const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', '
export const allConcreteAnswerOptionsExhausted = ( export const allConcreteAnswerOptionsExhausted = (
posts: Post[], posts: Post[],
question: GekanatorQuestion | null, question: GekanatorQuestion | null): boolean => {
): boolean => {
if (!(question)) if (!(question))
return false return false
@@ -125,8 +122,7 @@ export const recoverCandidatePosts = (
recoveredCandidatePosts: Map<number, RecoveredCandidateState> recoveredCandidatePosts: Map<number, RecoveredCandidateState>
eligiblePostIds: Set<number> eligiblePostIds: Set<number>
answerCountAtRecovery: number answerCountAtRecovery: number
recoveryStepCount: number }, recoveryStepCount: number }): { recoveredCandidatePosts: Map<number, RecoveredCandidateState>
): { recoveredCandidatePosts: Map<number, RecoveredCandidateState>
recoveryStepCount: number } | null => { recoveryStepCount: number } | null => {
const recovered = new Map (recoveredCandidatePosts) const recovered = new Map (recoveredCandidatePosts)
const targetSize = nextRecoveryTargetSize (recoveryStepCount) const targetSize = nextRecoveryTargetSize (recoveryStepCount)
+8 -16
ファイルの表示
@@ -8,8 +8,7 @@ import type {
export const monthForCondition = ( export const monthForCondition = (
condition: GekanatorQuestion['condition'], condition: GekanatorQuestion['condition']): number | null => {
): number | null => {
if (condition.type === 'original-month') if (condition.type === 'original-month')
return condition.month return condition.month
@@ -24,8 +23,7 @@ export const monthForCondition = (
const isTitleLengthContradiction = ( const isTitleLengthContradiction = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'],
answer: GekanatorAnswerValue, answer: GekanatorAnswerValue): boolean => {
): boolean => {
const candidateLength = titleLengthMinimumForCondition (candidate) const candidateLength = titleLengthMinimumForCondition (candidate)
const previousLength = titleLengthMinimumForCondition (previous) const previousLength = titleLengthMinimumForCondition (previous)
if (candidateLength === null || previousLength === null) if (candidateLength === null || previousLength === null)
@@ -45,8 +43,7 @@ const isTitleLengthContradiction = (
const isQuestionRedundantAfterAnswers = ( const isQuestionRedundantAfterAnswers = (
question: GekanatorQuestion, question: GekanatorQuestion,
answers: GekanatorAnswerLog[], answers: GekanatorAnswerLog[]): boolean => answers.some (answer => {
): boolean => answers.some (answer => {
const previous = answer.questionCondition const previous = answer.questionCondition
return previous !== undefined return previous !== undefined
&& isTitleLengthContradiction (question.condition, previous, answer.answer) && isTitleLengthContradiction (question.condition, previous, answer.answer)
@@ -56,8 +53,7 @@ const isQuestionRedundantAfterAnswers = (
const isSourceFactBlocked = ( const isSourceFactBlocked = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'],
answer: GekanatorAnswerValue, answer: GekanatorAnswerValue): boolean => {
): boolean => {
if (candidate.type !== 'source' || previous.type !== 'source') if (candidate.type !== 'source' || previous.type !== 'source')
return false return false
@@ -76,8 +72,7 @@ const isSourceFactBlocked = (
const isOriginalYearFactBlocked = ( const isOriginalYearFactBlocked = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'],
answer: GekanatorAnswerValue, answer: GekanatorAnswerValue): boolean => {
): boolean => {
if (candidate.type !== 'original-year' || previous.type !== 'original-year') if (candidate.type !== 'original-year' || previous.type !== 'original-year')
return false return false
@@ -96,8 +91,7 @@ const isOriginalYearFactBlocked = (
const isOriginalMonthFactBlocked = ( const isOriginalMonthFactBlocked = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'],
answer: GekanatorAnswerValue, answer: GekanatorAnswerValue): boolean => {
): boolean => {
switch (answer) switch (answer)
{ {
case 'yes': case 'yes':
@@ -143,8 +137,7 @@ const isOriginalMonthFactBlocked = (
const isFactQuestionBlocked = ( const isFactQuestionBlocked = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'],
answer: GekanatorAnswerValue, answer: GekanatorAnswerValue): boolean => {
): boolean => {
if (!(answer === 'yes' || answer === 'no')) if (!(answer === 'yes' || answer === 'no'))
return false return false
@@ -156,8 +149,7 @@ const isFactQuestionBlocked = (
export const isQuestionHardFilteredAfterAnswers = ( export const isQuestionHardFilteredAfterAnswers = (
question: GekanatorQuestion, question: GekanatorQuestion,
answers: GekanatorAnswerLog[], answers: GekanatorAnswerLog[]): boolean => answers.some (answer => {
): boolean => answers.some (answer => {
const previous = answer.questionCondition const previous = answer.questionCondition
if (previous === undefined) if (previous === undefined)
return false return false
+8 -16
ファイルの表示
@@ -29,8 +29,7 @@ const MATERIAL_FILTERS: MaterialFilter[] = ['present', 'missing', 'any']
export const parseMaterialFilter = ( export const parseMaterialFilter = (
value: unknown, value: unknown,
fallback: MaterialFilter = 'present', fallback: MaterialFilter = 'present'): MaterialFilter =>
): MaterialFilter =>
typeof value === 'string' && MATERIAL_FILTERS.includes (value as MaterialFilter) typeof value === 'string' && MATERIAL_FILTERS.includes (value as MaterialFilter)
? value as MaterialFilter ? value as MaterialFilter
: fallback : fallback
@@ -38,8 +37,7 @@ export const parseMaterialFilter = (
export const fetchMaterials = async ( export const fetchMaterials = async (
{ q, tagState, mediaKind, suppression, createdFrom, createdTo, { q, tagState, mediaKind, suppression, createdFrom, createdTo,
updatedFrom, updatedTo, sort, direction, page, limit }: FetchMaterialsParams, updatedFrom, updatedTo, sort, direction, page, limit }: FetchMaterialsParams): Promise<MaterialIndexResponse> =>
): Promise<MaterialIndexResponse> =>
await apiGet ('/materials', { params: { await apiGet ('/materials', { params: {
...(q && { q }), ...(q && { q }),
tag_state: tagState, tag_state: tagState,
@@ -52,8 +50,7 @@ export const fetchMaterials = async (
sort, sort,
direction, direction,
page, page,
limit, limit} })
} })
export const fetchMaterial = async (id: string): Promise<Material | null> => { export const fetchMaterial = async (id: string): Promise<Material | null> => {
@@ -71,18 +68,15 @@ export const fetchMaterial = async (id: string): Promise<Material | null> => {
export const fetchMaterialTagTree = async ( export const fetchMaterialTagTree = async (
{ parentId, materialFilter }: FetchMaterialTreeParams, { parentId, materialFilter }: FetchMaterialTreeParams): Promise<MaterialSidebarTag[]> =>
): Promise<MaterialSidebarTag[]> =>
await apiGet ('/tags/with-depth', { params: { await apiGet ('/tags/with-depth', { params: {
...(parentId != null && { parent: String (parentId) }), ...(parentId != null && { parent: String (parentId) }),
material_filter: materialFilter, material_filter: materialFilter} })
} })
export const fetchMaterialTagByName = async ( export const fetchMaterialTagByName = async (
name: string, name: string,
materialFilter: MaterialFilter, materialFilter: MaterialFilter): Promise<MaterialTagTree | null> => {
): Promise<MaterialTagTree | null> => {
try try
{ {
return await apiGet (`/tags/name/${ encodeURIComponent (name) }/materials`, return await apiGet (`/tags/name/${ encodeURIComponent (name) }/materials`,
@@ -103,13 +97,11 @@ export const createMaterial = async (formData: FormData): Promise<Material> =>
export const updateMaterial = async ( export const updateMaterial = async (
id: string, id: string,
formData: FormData, formData: FormData): Promise<Material> =>
): Promise<Material> =>
await apiPut (`/materials/${ id }`, formData) await apiPut (`/materials/${ id }`, formData)
export const suppressMaterialFile = async ( export const suppressMaterialFile = async (
id: string, id: string,
payload: { reason: string; purge?: boolean }, payload: { reason: string; purge?: boolean }): Promise<Material> =>
): Promise<Material> =>
await apiPatch (`/materials/${ id }/suppress_file`, payload) await apiPatch (`/materials/${ id }/suppress_file`, payload)
+3 -6
ファイルの表示
@@ -5,8 +5,7 @@ import type { FetchPostsParams, Post, PostVersion } from '@/types'
export const fetchPosts = async ( export const fetchPosts = async (
{ url, title, tags, match, createdFrom, createdTo, updatedFrom, updatedTo, { url, title, tags, match, createdFrom, createdTo, updatedFrom, updatedTo,
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams, originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams): Promise<{
): Promise<{
posts: Post[] posts: Post[]
count: number }> => count: number }> =>
await apiGet ('/posts', { params: { await apiGet ('/posts', { params: {
@@ -33,8 +32,7 @@ export const fetchPostChanges = async (
post?: string post?: string
tag?: string tag?: string
page: number page: number
limit: number }, limit: number }): Promise<{
): Promise<{
versions: PostVersion[] versions: PostVersion[]
count: number }> => count: number }> =>
await apiGet ('/posts/versions', { params: { ...(post && { post }), await apiGet ('/posts/versions', { params: { ...(post && { post }),
@@ -52,8 +50,7 @@ export const updatePost = async (
{ baseVersionNo, force, merge }: { { baseVersionNo, force, merge }: {
baseVersionNo?: number baseVersionNo?: number
force?: boolean force?: boolean
merge?: boolean } merge?: boolean }) =>
) =>
await apiPut<Post> ( await apiPut<Post> (
`/posts/${ post.id }`, `/posts/${ post.id }`,
{ title: post.title, { title: post.title,
+1 -2
ファイルの表示
@@ -42,8 +42,7 @@ export const materialsKeys = {
materialFilter: MaterialFilter materialFilter: MaterialFilter
}) => ['materials', 'tree', p] as const, }) => ['materials', 'tree', p] as const,
unclassified: (p: { page?: number; limit?: number } = { }) => unclassified: (p: { page?: number; limit?: number } = { }) =>
['materials', 'unclassified', p] as const, ['materials', 'unclassified', p] as const}
}
export const wikiKeys = { export const wikiKeys = {
root: ['wiki'] as const, root: ['wiki'] as const,
+4 -8
ファイルの表示
@@ -11,8 +11,7 @@ import type { Deerjikist,
export const fetchTags = async ( export const fetchTags = async (
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo, { post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
updatedFrom, updatedTo, deprecated, updatedFrom, updatedTo, deprecated,
page, limit, order }: FetchTagsParams, page, limit, order }: FetchTagsParams): Promise<{ tags: Tag[]
): Promise<{ tags: Tag[]
count: number }> => count: number }> =>
await apiGet ('/tags', { params: { await apiGet ('/tags', { params: {
...(post != null && { post }), ...(post != null && { post }),
@@ -31,8 +30,7 @@ export const fetchTags = async (
export const fetchNicoTags = async ( export const fetchNicoTags = async (
{ name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams, { name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams): Promise<{ tags: NicoTag[]
): Promise<{ tags: NicoTag[]
count: number }> => count: number }> =>
await apiGet ('/tags/nico', { params: { await apiGet ('/tags/nico', { params: {
page, page,
@@ -70,14 +68,12 @@ export const fetchTagChanges = async (
{ id, page, limit }: { { id, page, limit }: {
id?: string id?: string
page: number page: number
limit: number }, limit: number }): Promise<{
): Promise<{
versions: TagVersion[] versions: TagVersion[]
count: number }> => count: number }> =>
await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } }) await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } })
export const fetchDeerjikistsByTag = async ( export const fetchDeerjikistsByTag = async (
id: string, id: string): Promise<{ tag: Tag; deerjikists: Deerjikist[] }> =>
): Promise<{ tag: Tag; deerjikists: Deerjikist[]}> =>
await apiGet (`/tags/${ id }/deerjikists`) await apiGet (`/tags/${ id }/deerjikists`)
+1 -2
ファイルの表示
@@ -4,5 +4,4 @@ const CONTENT_EDITOR_ROLES: readonly UserRole[] = ['admin', 'member']
export const canEditContent = ( export const canEditContent = (
user: Pick<User, 'role'> | null | undefined, user: Pick<User, 'role'> | null | undefined): boolean => user != null && CONTENT_EDITOR_ROLES.includes (user.role)
): boolean => user != null && CONTENT_EDITOR_ROLES.includes (user.role)
+2 -4
ファイルの表示
@@ -12,8 +12,7 @@ export const cn = (...inputs: ClassValue[]) => twMerge (clsx (...inputs))
export const dateString = ( export const dateString = (
d: string | Date, d: string | Date,
unknown: 'month' | 'day' | 'hour' | 'minute' | 'second' | null = null, unknown: 'month' | 'day' | 'hour' | 'minute' | 'second' | null = null): string =>
): string =>
toDate (d).toLocaleString ( toDate (d).toLocaleString (
'ja-JP-u-ca-japanese', 'ja-JP-u-ca-japanese',
{ era: 'long', { era: 'long',
@@ -28,8 +27,7 @@ export const dateString = (
export const originalCreatedAtString = ( export const originalCreatedAtString = (
f: string | Date | null, f: string | Date | null,
b: string | Date | null, b: string | Date | null): string => {
): string => {
const from = f ? toDate (f) : null const from = f ? toDate (f) : null
const before = b ? toDate (b) : null const before = b ? toDate (b) : null
+3 -6
ファイルの表示
@@ -4,22 +4,19 @@ import type { WikiPage } from '@/types'
export const fetchWikiPages = async ( export const fetchWikiPages = async (
{ title }: { title?: string }, { title }: { title?: string }): Promise<WikiPage[]> =>
): Promise<WikiPage[]> =>
await apiGet ('/wiki', { params: { title } }) await apiGet ('/wiki', { params: { title } })
export const fetchWikiPage = async ( export const fetchWikiPage = async (
id: string, id: string,
{ version }: { version?: string }, { version }: { version?: string }): Promise<WikiPage> =>
): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } }) await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } })
export const fetchWikiPageByTitle = async ( export const fetchWikiPageByTitle = async (
title: string, title: string,
{ version }: { version?: string }, { version }: { version?: string }): Promise<WikiPage | null> => {
): Promise<WikiPage | null> => {
try try
{ {
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } }) return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
+73 -146
ファイルの表示
@@ -238,8 +238,7 @@ const createGameSeed = (): string => {
const normalizeStoredQuestionId = ( const normalizeStoredQuestionId = (
questionId: string, questionId: string,
condition?: GekanatorQuestionCondition, condition?: GekanatorQuestionCondition): string => {
): string => {
if (condition?.type === 'title-length-greater-than') if (condition?.type === 'title-length-greater-than')
return `title:length-at-least:${ condition.length + 1 }` return `title:length-at-least:${ condition.length + 1 }`
@@ -312,8 +311,7 @@ const sourcePriorityForMerge = (question: GekanatorQuestion): number => {
const shouldReplaceMergedQuestion = ( const shouldReplaceMergedQuestion = (
current: GekanatorQuestion | undefined, current: GekanatorQuestion | undefined,
candidate: GekanatorQuestion, candidate: GekanatorQuestion): boolean => {
): boolean => {
if (!(current)) if (!(current))
return true return true
@@ -408,8 +406,7 @@ const loadRecentGames = (): RecentGameSummary[] => {
const storeRecentGameSummary = ( const storeRecentGameSummary = (
summary: RecentGameSummary, summary: RecentGameSummary): RecentGameSummary[] => {
): RecentGameSummary[] => {
const next = const next =
[summary, [summary,
...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt ...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt
@@ -459,8 +456,7 @@ const resettableExtraQuestionState = (): {
const recoveredCandidateMapFromStored = ( const recoveredCandidateMapFromStored = (
items: RecoveredCandidatePost[], items: RecoveredCandidatePost[],
scores: [number, number][], scores: [number, number][]): Map<number, RecoveredCandidateState> => {
): Map<number, RecoveredCandidateState> => {
const storedScores = new Map (scores) const storedScores = new Map (scores)
return new Map (items.map (item => [item.postId, { return new Map (items.map (item => [item.postId, {
@@ -470,8 +466,7 @@ const recoveredCandidateMapFromStored = (
const storedRecoveredCandidatesFromMap = ( const storedRecoveredCandidatesFromMap = (
recoveredCandidatePosts: Map<number, RecoveredCandidateState>, recoveredCandidatePosts: Map<number, RecoveredCandidateState>): RecoveredCandidatePost[] =>
): RecoveredCandidatePost[] =>
[...recoveredCandidatePosts.entries ()] [...recoveredCandidatePosts.entries ()]
.map (([postId, recoveredCandidate]) => ({ .map (([postId, recoveredCandidate]) => ({
postId, postId,
@@ -513,8 +508,7 @@ const distributionEntropy = (weights: number[]): number =>
const questionCategoryPenalty = ( const questionCategoryPenalty = (
question: GekanatorQuestion, question: GekanatorQuestion,
answerCount: number, answerCount: number,
repeatPenalty: number, repeatPenalty: number): number => {
): number => {
const earlyFactor = Math.max (0, (3 - answerCount) / 3) const earlyFactor = Math.max (0, (3 - answerCount) / 3)
const titleLengthPenalty = (() => { const titleLengthPenalty = (() => {
if (titleLengthMinimumForCondition (question.condition) == null) if (titleLengthMinimumForCondition (question.condition) == null)
@@ -553,8 +547,7 @@ const relatedPostIdsOf = (post: Post): number[] => {
const userPriorWeightsFor = ( const userPriorWeightsFor = (
posts: Post[], posts: Post[],
recentGames: RecentGameSummary[], recentGames: RecentGameSummary[]): Map<number, number> => {
): Map<number, number> => {
const postById = new Map (posts.map (post => [post.id, post])) const postById = new Map (posts.map (post => [post.id, post]))
const weights = new Map<number, number> () const weights = new Map<number, number> ()
const addWeight = (postId: number, weight: number) => { const addWeight = (postId: number, weight: number) => {
@@ -581,14 +574,12 @@ const userPriorWeightsFor = (
const answerWeightFor = ( const answerWeightFor = (
questionId: string, questionId: string,
softenedQuestionIds: Set<string>, softenedQuestionIds: Set<string>): number => softenedQuestionIds.has (questionId) ? softenedAnswerWeight : 1
): number => softenedQuestionIds.has (questionId) ? softenedAnswerWeight : 1
const scoreWeightForAnswer = ( const scoreWeightForAnswer = (
answer: GekanatorAnswerLog, answer: GekanatorAnswerLog,
softenedQuestionIds: Set<string>, softenedQuestionIds: Set<string>): number =>
): number =>
answerWeightFor (answer.questionId, softenedQuestionIds) answerWeightFor (answer.questionId, softenedQuestionIds)
* ( * (
answer.questionPurpose === 'learning_user_suggested' answer.questionPurpose === 'learning_user_suggested'
@@ -638,8 +629,7 @@ const titleTermPattern =
const addPostIdToIndex = <K extends string | number> ( const addPostIdToIndex = <K extends string | number> (
index: Map<K, Set<number>>, index: Map<K, Set<number>>,
key: K, key: K,
postId: number, postId: number) => {
) => {
const current = index.get (key) const current = index.get (key)
if (current) if (current)
{ {
@@ -652,8 +642,7 @@ const addPostIdToIndex = <K extends string | number> (
const buildMaterialIndex = ( const buildMaterialIndex = (
posts: Post[], posts: Post[]): GekanatorQuestionMaterialIndex => {
): GekanatorQuestionMaterialIndex => {
const postById = new Map<number, Post> () const postById = new Map<number, Post> ()
const tagKeysByPostId = new Map<number, string[]> () const tagKeysByPostId = new Map<number, string[]> ()
const postIdsByTagKey = new Map<string, Set<number>> () const postIdsByTagKey = new Map<string, Set<number>> ()
@@ -784,8 +773,7 @@ const originalDateQuestionTextFor = (
condition: Extract< condition: Extract<
GekanatorQuestionCondition, GekanatorQuestionCondition,
{ type: 'original-year' | 'original-month' | 'original-month-day' } { type: 'original-year' | 'original-month' | 'original-month-day' }
>, >): string => {
): string => {
switch (condition.type) switch (condition.type)
{ {
case 'original-year': case 'original-year':
@@ -852,8 +840,7 @@ const isLearnableTagKey = (key: string): boolean => !(key.startsWith ('nico:'))
const isUserSuggestedLearnedSemanticQuestion = ( const isUserSuggestedLearnedSemanticQuestion = (
question: GekanatorQuestion, question: GekanatorQuestion): boolean => isLearnedSemanticQuestion (question)
): boolean => isLearnedSemanticQuestion (question)
type LearnedSemanticCandidateStats = { type LearnedSemanticCandidateStats = {
@@ -872,8 +859,7 @@ const learnedSemanticStatsForCandidateIds = (
question }: { question }: {
candidateIds: number[] candidateIds: number[]
posts: Post[] posts: Post[]
question: GekanatorQuestion }, question: GekanatorQuestion }): LearnedSemanticCandidateStats => {
): LearnedSemanticCandidateStats => {
const candidateIdSet = new Set (candidateIds) const candidateIdSet = new Set (candidateIds)
const positiveIds = new Set<number> () const positiveIds = new Set<number> ()
const negativeIds = new Set<number> () const negativeIds = new Set<number> ()
@@ -915,8 +901,7 @@ const learnedSemanticQuestionIsEffectiveForCandidateIds = (
question }: { question }: {
candidateIds: number[] candidateIds: number[]
posts: Post[] posts: Post[]
question: GekanatorQuestion }, question: GekanatorQuestion }): boolean => {
): boolean => {
if (!(isUserSuggestedLearnedSemanticQuestion (question))) if (!(isUserSuggestedLearnedSemanticQuestion (question)))
return false return false
@@ -936,8 +921,7 @@ const learnedSemanticQuestionIsEffectiveForCandidateIds = (
const directSemanticAnswerForPost = ( const directSemanticAnswerForPost = (
question: GekanatorQuestion, question: GekanatorQuestion,
post: Post, post: Post): GekanatorAnswerValue | null => {
): GekanatorAnswerValue | null => {
const direct = question.exampleAnswers?.[String (post.id) as `${ number }`] const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
if (direct) if (direct)
return direct return direct
@@ -956,8 +940,7 @@ const learnedSemanticLearningValueForTopPosts = (
question: GekanatorQuestion question: GekanatorQuestion
learningTargetPosts: Post[] learningTargetPosts: Post[]
candidateIds: number[] candidateIds: number[]
posts: Post[] }, posts: Post[] }): { missingTopCount: number
): { missingTopCount: number
knownCount: number knownCount: number
hasLearningValue: boolean } => { hasLearningValue: boolean } => {
const missingTopCount = const missingTopCount =
@@ -1000,8 +983,7 @@ const learningTargetPostsForCandidates = ({
const questionPurposeCountsFor = ( const questionPurposeCountsFor = (
answers: GekanatorAnswerLog[], answers: GekanatorAnswerLog[]): {
): {
effectiveUserSuggestedCount: number effectiveUserSuggestedCount: number
learningUserSuggestedCount: number learningUserSuggestedCount: number
normalQuestionCount: number normalQuestionCount: number
@@ -1041,8 +1023,7 @@ const questionPurposeCountsFor = (
const learnedSemanticNarrowPenaltyForStats = ( const learnedSemanticNarrowPenaltyForStats = (
candidateCount: number, candidateCount: number,
stats: LearnedSemanticCandidateStats, stats: LearnedSemanticCandidateStats): number => {
): number => {
const minSide = candidateCount < 10 ? 1 : Math.max (3, candidateCount * .08) const minSide = candidateCount < 10 ? 1 : Math.max (3, candidateCount * .08)
return stats.positiveCount < minSide || stats.negativeCount < minSide ? .15 : 0 return stats.positiveCount < minSide || stats.negativeCount < minSide ? .15 : 0
} }
@@ -1050,8 +1031,7 @@ const learnedSemanticNarrowPenaltyForStats = (
const learnedSemanticScoreDeltaForExpectedAnswer = ( const learnedSemanticScoreDeltaForExpectedAnswer = (
userAnswer: GekanatorAnswerValue, userAnswer: GekanatorAnswerValue,
expectedAnswer: GekanatorAnswerValue | null, expectedAnswer: GekanatorAnswerValue | null): number => {
): number => {
switch (userAnswer) switch (userAnswer)
{ {
case 'yes': case 'yes':
@@ -1089,8 +1069,7 @@ const learnedSemanticScoreDeltaForExpectedAnswer = (
const scoreDropDeltaForRecoveredPost = ( const scoreDropDeltaForRecoveredPost = (
postId: number, postId: number,
totalScore: number, totalScore: number,
recoveredCandidatePosts: Map<number, RecoveredCandidateState>, recoveredCandidatePosts: Map<number, RecoveredCandidateState>): number => {
): number => {
const recoveredCandidate = recoveredCandidatePosts.get (postId) const recoveredCandidate = recoveredCandidatePosts.get (postId)
if (recoveredCandidate == null) if (recoveredCandidate == null)
return totalScore return totalScore
@@ -1114,8 +1093,7 @@ const postPassesScoreDrop = (
recoveredCandidatePosts }: { recoveredCandidatePosts }: {
postId: number postId: number
scores: Map<number, number> scores: Map<number, number>
recoveredCandidatePosts: Map<number, RecoveredCandidateState> }, recoveredCandidatePosts: Map<number, RecoveredCandidateState> }): boolean => {
): boolean => {
if (!(activeCandidateScoreDropEnabled (scores))) if (!(activeCandidateScoreDropEnabled (scores)))
return true return true
@@ -1129,8 +1107,7 @@ const postPassesScoreDrop = (
// `post_similarities` is the score-propagation graph, not the question kind. // `post_similarities` is the score-propagation graph, not the question kind.
const questionUsesPostSimilarityPropagationGraphForScoring = ( const questionUsesPostSimilarityPropagationGraphForScoring = (
question: GekanatorQuestion, question: GekanatorQuestion): boolean =>
): boolean =>
(question.kind === 'post_similarity' (question.kind === 'post_similarity'
&& !(isUserSuggestedLearnedSemanticQuestion (question))) && !(isUserSuggestedLearnedSemanticQuestion (question)))
|| (question.kind === 'tag' || (question.kind === 'tag'
@@ -1139,8 +1116,7 @@ const questionUsesPostSimilarityPropagationGraphForScoring = (
const questionSupportsAnswerBasedHardFiltering = ( const questionSupportsAnswerBasedHardFiltering = (
question: GekanatorQuestion, question: GekanatorQuestion): boolean => !(questionUsesPostSimilarityPropagationGraphForScoring (question))
): boolean => !(questionUsesPostSimilarityPropagationGraphForScoring (question))
&& !(isUserSuggestedLearnedSemanticQuestion (question)) && !(isUserSuggestedLearnedSemanticQuestion (question))
@@ -1160,8 +1136,7 @@ const usesLearnedTagExamples = (question: GekanatorQuestion): boolean =>
const searchedQuestionsFor = ( const searchedQuestionsFor = (
questions: GekanatorQuestion[], questions: GekanatorQuestion[],
search: string, search: string): GekanatorQuestion[] => {
): GekanatorQuestion[] => {
const needle = search.trim () const needle = search.trim ()
if (!(needle)) if (!(needle))
return [] return []
@@ -1235,8 +1210,7 @@ type QuestionMatchResolver = {
const buildGekanatorMatchIndex = ( const buildGekanatorMatchIndex = (
posts: Post[], posts: Post[],
questions: GekanatorQuestion[], questions: GekanatorQuestion[]): GekanatorMatchIndex => new Map (
): GekanatorMatchIndex => new Map (
questions.map (question => [ questions.map (question => [
question.id, question.id,
new Set ( new Set (
@@ -1285,8 +1259,7 @@ const matchingPostIdsForQuestion = ({
const positiveMatchingPostIdsForQuestion = ( const positiveMatchingPostIdsForQuestion = (
resolver: QuestionMatchResolver, resolver: QuestionMatchResolver): Set<number> => {
): Set<number> => {
if (isUserSuggestedLearnedSemanticQuestion (resolver.question)) if (isUserSuggestedLearnedSemanticQuestion (resolver.question))
{ {
const cached = resolver.dynamicMatchIndex?.get (resolver.question.id) const cached = resolver.dynamicMatchIndex?.get (resolver.question.id)
@@ -1356,8 +1329,7 @@ const matchingWeightInCandidates = (
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex matchIndex: GekanatorMatchIndex
question: GekanatorQuestion question: GekanatorQuestion
dynamicMatchIndex?: GekanatorMatchIndex }, dynamicMatchIndex?: GekanatorMatchIndex }): number => {
): number => {
const matched = positiveMatchingPostIdsForQuestion ({ const matched = positiveMatchingPostIdsForQuestion ({
posts, posts,
materialIndex, materialIndex,
@@ -1380,8 +1352,7 @@ const signatureForCandidateIds = (
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex matchIndex: GekanatorMatchIndex
question: GekanatorQuestion question: GekanatorQuestion
dynamicMatchIndex?: GekanatorMatchIndex }, dynamicMatchIndex?: GekanatorMatchIndex }): string => {
): string => {
if (isUserSuggestedLearnedSemanticQuestion (question)) if (isUserSuggestedLearnedSemanticQuestion (question))
{ {
const postById = new Map (posts.map (post => [post.id, post])) const postById = new Map (posts.map (post => [post.id, post]))
@@ -1421,8 +1392,7 @@ const postIdsForHardAnswer = (
posts: Post[] posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex matchIndex: GekanatorMatchIndex
dynamicMatchIndex?: GekanatorMatchIndex }, dynamicMatchIndex?: GekanatorMatchIndex }): number[] => {
): number[] => {
if (!(questionSupportsAnswerBasedHardFiltering (question))) if (!(questionSupportsAnswerBasedHardFiltering (question)))
return candidateIds return candidateIds
@@ -1562,8 +1532,7 @@ const buildIndexedQuestion = (
text: string text: string
kind: GekanatorQuestionKind kind: GekanatorQuestionKind
priorityWeight: number priorityWeight: number
materialIndex: GekanatorQuestionMaterialIndex }, materialIndex: GekanatorQuestionMaterialIndex }): GekanatorQuestion => ({
): GekanatorQuestion => ({
id: questionIdForCondition (condition), id: questionIdForCondition (condition),
text, text,
kind, kind,
@@ -1578,8 +1547,7 @@ const buildIndexedQuestion = (
const rankedEntriesForCounts = <T extends string | number> ( const rankedEntriesForCounts = <T extends string | number> (
{ counts, total, cap }: { counts: Map<T, number> { counts, total, cap }: { counts: Map<T, number>
total: number total: number
cap: number }, cap: number }): [T, number][] =>
): [T, number][] =>
([...counts.entries ()] ([...counts.entries ()]
.filter (([, count]) => count > 0 && count < total) .filter (([, count]) => count > 0 && count < total)
.sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1])) .sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1]))
@@ -1594,8 +1562,7 @@ const buildQuestionsForCandidateIds = (
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
acceptedQuestions: GekanatorQuestion[] acceptedQuestions: GekanatorQuestion[]
mode?: QuestionBuildMode mode?: QuestionBuildMode
confirmationPostId?: number | null }, confirmationPostId?: number | null }): GekanatorQuestion[] => {
): GekanatorQuestion[] => {
const total = candidateIds.length const total = candidateIds.length
const confirmationPost = (() => { const confirmationPost = (() => {
if (confirmationPostId == null) if (confirmationPostId == null)
@@ -1652,8 +1619,7 @@ const buildQuestionsForCandidateIds = (
condition: Extract< condition: Extract<
GekanatorQuestionCondition, GekanatorQuestionCondition,
{ type: 'original-year' | 'original-month' | 'original-month-day' } { type: 'original-year' | 'original-month' | 'original-month-day' }
>, >): GekanatorQuestion => {
): GekanatorQuestion => {
const priorityWeight = (() => { const priorityWeight = (() => {
if (condition.type === 'original-year') if (condition.type === 'original-year')
return 1.04 return 1.04
@@ -1673,8 +1639,7 @@ const buildQuestionsForCandidateIds = (
const specialMonthDays = rankedEntriesForCounts ({ const specialMonthDays = rankedEntriesForCounts ({
counts: monthDayCounts, counts: monthDayCounts,
total, total,
cap: factCap cap: factCap}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null)
}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null)
if (mode === 'split') if (mode === 'split')
{ {
@@ -2124,8 +2089,7 @@ type ExclusiveConditionGroup =
const exclusiveConditionGroupFor = ( const exclusiveConditionGroupFor = (
condition: GekanatorQuestion['condition'], condition: GekanatorQuestion['condition']): ExclusiveConditionGroup | null => {
): ExclusiveConditionGroup | null => {
switch (condition.type) switch (condition.type)
{ {
case 'original-month': case 'original-month':
@@ -2144,8 +2108,7 @@ const exclusiveConditionGroupFor = (
const sameConditionValue = ( const sameConditionValue = (
left: GekanatorQuestion['condition'], left: GekanatorQuestion['condition'],
right: GekanatorQuestion['condition'], right: GekanatorQuestion['condition']): boolean => {
): boolean => {
const leftTitleLength = titleLengthMinimumForCondition (left) const leftTitleLength = titleLengthMinimumForCondition (left)
const rightTitleLength = titleLengthMinimumForCondition (right) const rightTitleLength = titleLengthMinimumForCondition (right)
if (leftTitleLength != null || rightTitleLength != null) if (leftTitleLength != null || rightTitleLength != null)
@@ -2187,8 +2150,7 @@ const sameConditionValue = (
const isMonthCrossMatch = ( const isMonthCrossMatch = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition']): boolean => {
): boolean => {
const candidateMonth = monthForCondition (candidate) const candidateMonth = monthForCondition (candidate)
const previousMonth = monthForCondition (previous) const previousMonth = monthForCondition (previous)
if (candidateMonth == null || previousMonth == null) if (candidateMonth == null || previousMonth == null)
@@ -2204,8 +2166,7 @@ const isMonthCrossMatch = (
const isExclusiveContradiction = ( const isExclusiveContradiction = (
candidate: GekanatorQuestion['condition'], candidate: GekanatorQuestion['condition'],
previous: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition']): boolean => {
): boolean => {
const candidateGroup = exclusiveConditionGroupFor (candidate) const candidateGroup = exclusiveConditionGroupFor (candidate)
const previousGroup = exclusiveConditionGroupFor (previous) const previousGroup = exclusiveConditionGroupFor (previous)
@@ -2242,16 +2203,14 @@ const contradictionPenaltyFor = ({
case 'no': case 'no':
if ( if (
sameConditionValue (question.condition, previous) sameConditionValue (question.condition, previous)
|| isMonthCrossMatch (question.condition, previous) || isMonthCrossMatch (question.condition, previous))
)
return sum + 40 return sum + 40
return sum return sum
case 'probably_no': case 'probably_no':
if ( if (
sameConditionValue (question.condition, previous) sameConditionValue (question.condition, previous)
|| isMonthCrossMatch (question.condition, previous) || isMonthCrossMatch (question.condition, previous))
)
return sum + 20 return sum + 20
return sum return sum
@@ -2281,8 +2240,7 @@ const chooseQuestion = (
recentFirstQuestionPenaltyById: Map<string, number> recentFirstQuestionPenaltyById: Map<string, number>
userPriorWeights: Map<number, number> userPriorWeights: Map<number, number>
materialIndex: GekanatorQuestionMaterialIndex materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex }, matchIndex: GekanatorMatchIndex }): QuestionSelection | null => {
): QuestionSelection | null => {
const dynamicMatchIndex = new Map<string, Set<number>> () const dynamicMatchIndex = new Map<string, Set<number>> ()
const invertedSignature = (signature: string): string => const invertedSignature = (signature: string): string =>
@@ -2327,8 +2285,7 @@ const chooseQuestion = (
const rank = ( const rank = (
questionsToRank: GekanatorQuestion[], questionsToRank: GekanatorQuestion[],
candidates: { post: Post; score: number }[], candidates: { post: Post; score: number }[],
weightedCandidates: { post: Post; score: number; weight: number }[], weightedCandidates: { post: Post; score: number; weight: number }[]) => {
) => {
const redundant = redundantSignatures (candidates.map (item => item.post)) const redundant = redundantSignatures (candidates.map (item => item.post))
const candidateById = new Map (candidates.map (item => [item.post.id, item.post])) const candidateById = new Map (candidates.map (item => [item.post.id, item.post]))
const candidateIds = candidates.map (item => item.post.id) const candidateIds = candidates.map (item => item.post.id)
@@ -2613,8 +2570,7 @@ const chooseQuestion = (
if ( if (
effectiveRatio < targetEffectiveUserSuggestedQuestionRatio effectiveRatio < targetEffectiveUserSuggestedQuestionRatio
&& totalUserSuggestedRatio < targetTotalUserSuggestedQuestionRatio && totalUserSuggestedRatio < targetTotalUserSuggestedQuestionRatio
&& effectiveUserSuggestedPool.length > 0 && effectiveUserSuggestedPool.length > 0)
)
{ {
selectedPool = effectiveUserSuggestedPool selectedPool = effectiveUserSuggestedPool
selectedPurpose = 'effective_user_suggested' selectedPurpose = 'effective_user_suggested'
@@ -2622,8 +2578,7 @@ const chooseQuestion = (
else if ( else if (
learningRatio < targetLearningUserSuggestedQuestionRatio learningRatio < targetLearningUserSuggestedQuestionRatio
&& totalUserSuggestedRatio < targetTotalUserSuggestedQuestionRatio && totalUserSuggestedRatio < targetTotalUserSuggestedQuestionRatio
&& learningUserSuggestedPool.length > 0 && learningUserSuggestedPool.length > 0)
)
{ {
selectedPool = learningUserSuggestedPool selectedPool = learningUserSuggestedPool
selectedPurpose = 'learning_user_suggested' selectedPurpose = 'learning_user_suggested'
@@ -2683,8 +2638,7 @@ const chooseQuestion = (
const winningRunPriorityFor = ( const winningRunPriorityFor = (
expected: GekanatorAnswerValue, expected: GekanatorAnswerValue): number | null => {
): number | null => {
if (expected === 'yes') if (expected === 'yes')
return 0 return 0
if (expected === 'partial') if (expected === 'partial')
@@ -2719,8 +2673,7 @@ const chooseWinningRunQuestion = ({
materialIndex, materialIndex,
acceptedQuestions, acceptedQuestions,
mode: 'confirmation', mode: 'confirmation',
confirmationPostId: targetPost.id confirmationPostId: targetPost.id})
})
.filter (question => { .filter (question => {
if (askedIds.has (question.id)) if (askedIds.has (question.id))
return false return false
@@ -2829,8 +2782,7 @@ const chooseFallbackQuestion = ({
materialIndex, materialIndex,
acceptedQuestions: [], acceptedQuestions: [],
mode: 'confirmation', mode: 'confirmation',
confirmationPostId: post.id confirmationPostId: post.id})))
})))
.slice (0, 32) .slice (0, 32)
const dynamicMatchIndex = new Map<string, Set<number>> () const dynamicMatchIndex = new Map<string, Set<number>> ()
const ranked = mergeQuestions ([ const ranked = mergeQuestions ([
@@ -2905,8 +2857,7 @@ const chooseFallbackQuestion = ({
const shouldEnterGuessPhase = ( const shouldEnterGuessPhase = (
reason: GuessReason | null, reason: GuessReason | null): reason is 'hard_max_questions' | 'winning_run_finished' | 'question_count_checkpoint' =>
): reason is 'hard_max_questions' | 'winning_run_finished' | 'question_count_checkpoint' =>
(reason === 'hard_max_questions' (reason === 'hard_max_questions'
|| reason === 'winning_run_finished' || reason === 'winning_run_finished'
|| reason === 'question_count_checkpoint') || reason === 'question_count_checkpoint')
@@ -2914,15 +2865,13 @@ const shouldEnterGuessPhase = (
const isWinningRunActive = ( const isWinningRunActive = (
winningRunTargetId: number | null, winningRunTargetId: number | null,
winningRunStartAnswerCount: number | null, winningRunStartAnswerCount: number | null): boolean =>
): boolean =>
winningRunTargetId != null && winningRunStartAnswerCount != null winningRunTargetId != null && winningRunStartAnswerCount != null
const winningRunQuestionCount = ( const winningRunQuestionCount = (
answers: GekanatorAnswerLog[], answers: GekanatorAnswerLog[],
winningRunStartAnswerCount: number | null, winningRunStartAnswerCount: number | null): number => {
): number => {
if (winningRunStartAnswerCount == null) if (winningRunStartAnswerCount == null)
return 0 return 0
@@ -2962,8 +2911,7 @@ const nextQuestionPlanFor = (
matchIndex: GekanatorMatchIndex matchIndex: GekanatorMatchIndex
lastGuessQuestionCount: number lastGuessQuestionCount: number
winningRunTargetId: number | null winningRunTargetId: number | null
winningRunStartAnswerCount: number | null }, winningRunStartAnswerCount: number | null }): { question: GekanatorQuestion | null
): { question: GekanatorQuestion | null
guess: Post | null guess: Post | null
guessReason: GuessReason | null guessReason: GuessReason | null
questionMode: QuestionMode questionMode: QuestionMode
@@ -3013,8 +2961,7 @@ const nextQuestionPlanFor = (
if ( if (
isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount) isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
&& winningRunTargetId === nextWinningRunTargetId && winningRunTargetId === nextWinningRunTargetId
&& winningRunStartAnswerCount != null && winningRunStartAnswerCount != null)
)
return winningRunStartAnswerCount return winningRunStartAnswerCount
return answers.length return answers.length
@@ -3188,8 +3135,7 @@ const mascotStateFor = (
resultWon: boolean | null, resultWon: boolean | null,
eligiblePostCount: number, eligiblePostCount: number,
bestConfidencePercent: number, bestConfidencePercent: number,
winningRunActive: boolean, winningRunActive: boolean): MascotState => {
): MascotState => {
const resultPhase = const resultPhase =
phase === 'end' phase === 'end'
|| phase === 'review' || phase === 'review'
@@ -3210,13 +3156,11 @@ const mascotStateFor = (
if ( if (
winningRunActive winningRunActive
|| eligiblePostCount <= 2 || eligiblePostCount <= 2
|| bestConfidencePercent >= 70 || bestConfidencePercent >= 70)
)
return 'thinking_near' return 'thinking_near'
if ( if (
eligiblePostCount >= 15 eligiblePostCount >= 15
&& bestConfidencePercent < 45 && bestConfidencePercent < 45)
)
return 'thinking_far' return 'thinking_far'
return 'thinking_mid' return 'thinking_mid'
case 'guess': case 'guess':
@@ -3327,8 +3271,7 @@ const GekanatorBackdrop: FC<{
const settingsForMode = useCallback ( const settingsForMode = useCallback (
( (
mode: 'normal' | 'winning_run' | 'guess', mode: 'normal' | 'winning_run' | 'guess'): { columns: number; rows: number; opacity: number } => {
): { columns: number; rows: number; opacity: number } => {
if (mode === 'winning_run' || mode === 'guess') if (mode === 'winning_run' || mode === 'guess')
return { columns: 8, rows: 8, opacity: motionMode === 'calm' ? .18 : .24 } return { columns: 8, rows: 8, opacity: motionMode === 'calm' ? .18 : .24 }
@@ -3342,8 +3285,7 @@ const GekanatorBackdrop: FC<{
const scaleForMode = useCallback ( const scaleForMode = useCallback (
( (
mode: 'normal' | 'winning_run' | 'guess', mode: 'normal' | 'winning_run' | 'guess',
displayedWinningCount: number, displayedWinningCount: number): number => {
): number => {
if (mode === 'guess') if (mode === 'guess')
return 8 return 8
@@ -3355,8 +3297,7 @@ const GekanatorBackdrop: FC<{
[]) [])
const postsForMode = useCallback (( const postsForMode = useCallback ((
mode: 'normal' | 'winning_run' | 'guess', mode: 'normal' | 'winning_run' | 'guess'): Post[] => {
): Post[] => {
if (mode === 'guess' && displayedGuess) if (mode === 'guess' && displayedGuess)
return [displayedGuess] return [displayedGuess]
if (mode === 'winning_run' && winningRunTargetPost) if (mode === 'winning_run' && winningRunTargetPost)
@@ -3366,8 +3307,7 @@ const GekanatorBackdrop: FC<{
const thumbnailsForMode = useCallback (( const thumbnailsForMode = useCallback ((
mode: 'normal' | 'winning_run' | 'guess', mode: 'normal' | 'winning_run' | 'guess',
count: number, count: number): string[] => {
): string[] => {
const modePosts = postsForMode (mode) const modePosts = postsForMode (mode)
if (modePosts.length === 0) if (modePosts.length === 0)
return [] return []
@@ -3734,8 +3674,7 @@ const GekanatorBackdrop: FC<{
const expectedAnswerFor = ( const expectedAnswerFor = (
question: GekanatorQuestion | undefined, question: GekanatorQuestion | undefined,
correctPost: Post | null, correctPost: Post | null): GekanatorAnswerValue | null =>
): GekanatorAnswerValue | null =>
expectedAnswerForQuestion (question, correctPost) expectedAnswerForQuestion (question, correctPost)
@@ -4169,8 +4108,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setSaved (true) setSaved (true)
setSavedGameId (data.id) setSavedGameId (data.id)
setLearnedExampleCount (data.learnedExampleCount) setLearnedExampleCount (data.learnedExampleCount)
setResultWon (variables.guessedPostId === variables.correctPostId) setResultWon (variables.guessedPostId === variables.correctPostId)}})
}})
const questionSuggestionMutation = useMutation ({ const questionSuggestionMutation = useMutation ({
mutationFn: saveGekanatorQuestionSuggestion, mutationFn: saveGekanatorQuestionSuggestion,
onSuccess: async data => { onSuccess: async data => {
@@ -4180,15 +4118,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setQuestionSuggestionSearch ('') setQuestionSuggestionSearch ('')
setQuestionSuggestionSelectedId (null) setQuestionSuggestionSelectedId (null)
setQuestionSuggestion ('') setQuestionSuggestion ('')
setQuestionSuggestionAnswer ('yes') setQuestionSuggestionAnswer ('yes')}})
}})
const extraQuestionAnswersMutation = useMutation ({ const extraQuestionAnswersMutation = useMutation ({
mutationFn: saveGekanatorExtraQuestionAnswers, mutationFn: saveGekanatorExtraQuestionAnswers,
onSuccess: async () => { onSuccess: async () => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () }) await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
setExtraQuestionState ('saved') setExtraQuestionState ('saved')
setPhase ('end') setPhase ('end')}})
}})
const resetExtraQuestionState = () => { const resetExtraQuestionState = () => {
const next = resettableExtraQuestionState () const next = resettableExtraQuestionState ()
@@ -4330,8 +4266,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
if ( if (
!(allowPreQuestionRecovery) !(allowPreQuestionRecovery)
|| recoveredEligiblePosts.length === 0 || recoveredEligiblePosts.length === 0
|| recoveredEligiblePosts.length === 1 || recoveredEligiblePosts.length === 1)
)
return false return false
const nextQuestion = chooseQuestion ({ const nextQuestion = chooseQuestion ({
@@ -4493,8 +4428,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
if ( if (
!(nextPlan.question) !(nextPlan.question)
&& !(shouldEnterGuessPhase (nextPlan.guessReason)) && !(shouldEnterGuessPhase (nextPlan.guessReason))
&& recovered.eligiblePosts.length !== 1 && recovered.eligiblePosts.length !== 1)
)
{ {
const recoveredForQuestion = recoverQuestionState ({ const recoveredForQuestion = recoverQuestionState ({
nextAnswers, nextAnswers,
@@ -4602,8 +4536,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
!(canPersistGame) !(canPersistGame)
|| reviewGuessedPostId == null || reviewGuessedPostId == null
|| reviewCorrectPostId == null || reviewCorrectPostId == null
|| saveMutation.isPending || saveMutation.isPending)
)
return return
if (savedGameId != null) if (savedGameId != null)
@@ -4666,8 +4599,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
!(canPersistGame) !(canPersistGame)
|| savedGameId == null || savedGameId == null
|| extraQuestionAnswersMutation.isPending || extraQuestionAnswersMutation.isPending
|| extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)])) || extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)])))
)
return return
extraQuestionAnswersMutation.mutate ({ extraQuestionAnswersMutation.mutate ({
@@ -4870,8 +4802,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const answerExtraQuestion = ( const answerExtraQuestion = (
questionId: number, questionId: number,
value: GekanatorAnswerValue, value: GekanatorAnswerValue) => {
) => {
setExtraQuestionAnswers ({ setExtraQuestionAnswers ({
...extraQuestionAnswers, ...extraQuestionAnswers,
[String (questionId)]: value }) [String (questionId)]: value })
@@ -4909,8 +4840,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|| isLoading || isLoading
|| acceptedQuestionsLoading || acceptedQuestionsLoading
|| shouldEnterGuessPhase (questionPlan.guessReason) || shouldEnterGuessPhase (questionPlan.guessReason)
|| eligiblePosts.length === 1 || eligiblePosts.length === 1)
)
return return
const recovered = recoverQuestionState ({ const recovered = recoverQuestionState ({
@@ -4926,8 +4856,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
if ( if (
recovered.recoveryStepCount === recoveryStepCount recovered.recoveryStepCount === recoveryStepCount
&& recovered.recoveredCandidatePosts.size === recoveredCandidatePosts.size && recovered.recoveredCandidatePosts.size === recoveredCandidatePosts.size
&& recovered.softenedQuestionIds.size === softenedQuestionIds.size && recovered.softenedQuestionIds.size === softenedQuestionIds.size)
)
return return
setSoftenedQuestionIds (recovered.softenedQuestionIds) setSoftenedQuestionIds (recovered.softenedQuestionIds)
@@ -4954,15 +4883,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
if ( if (
phase !== 'question' phase !== 'question'
|| isLoading || isLoading
|| acceptedQuestionsLoading || acceptedQuestionsLoading)
)
return return
if ( if (
currentQuestion currentQuestion
|| !(questionPlan.guess) || !(questionPlan.guess)
|| !(shouldEnterGuessPhase (questionPlan.guessReason)) || !(shouldEnterGuessPhase (questionPlan.guessReason)))
)
return return
setWinningRunTargetId (questionPlan.winningRunTargetId) setWinningRunTargetId (questionPlan.winningRunTargetId)
+2 -4
ファイルの表示
@@ -145,8 +145,7 @@ const DeerjikistDetailPage: FC = () => {
return rtn return rtn
})}/>)} })}/>)}
</FormField> </FormField>
</fieldset> </fieldset>))}
))}
<div className="py-3"> <div className="py-3">
<button <button
@@ -169,8 +168,7 @@ const DeerjikistDetailPage: FC = () => {
</button> </button>
</div> </div>
</form> </form>
</div> </div>)}
)}
</MainArea>) </MainArea>)
} }
+5 -10
ファイルの表示
@@ -45,8 +45,7 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
const { data: material, isError, isLoading } = useQuery ({ const { data: material, isError, isLoading } = useQuery ({
queryKey: materialsKeys.show (id ?? ''), queryKey: materialsKeys.show (id ?? ''),
queryFn: () => fetchMaterial (id ?? ''), queryFn: () => fetchMaterial (id ?? ''),
enabled: id != null, enabled: id != null})
})
const materialTitle = material const materialTitle = material
? material.tag?.name ?? `素材 #${ material.id }` ? material.tag?.name ?? `素材 #${ material.id }`
: '' : ''
@@ -90,8 +89,7 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
onError: error => { onError: error => {
applyValidationError (error) applyValidationError (error)
toast ({ title: '更新失敗……', description: '入力を見直してください.' }) toast ({ title: '更新失敗……', description: '入力を見直してください.' })
}, }})
})
const suppressMutation = useMutation ({ const suppressMutation = useMutation ({
mutationFn: async (reason: string) => mutationFn: async (reason: string) =>
@@ -105,8 +103,7 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
}, },
onError: () => { onError: () => {
toast ({ title: '抑止に失敗しました' }) toast ({ title: '抑止に失敗しました' })
}, }})
})
const handleSubmit = () => { const handleSubmit = () => {
clearValidationErrors () clearValidationErrors ()
@@ -133,12 +130,10 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
{isLoading ? 'Loading...' : isError ? ( {isLoading ? 'Loading...' : isError ? (
<p className="text-red-600 dark:text-red-300"> <p className="text-red-600 dark:text-red-300">
</p> </p>) : material == null ? (
) : material == null ? (
<p className="text-stone-700 dark:text-stone-300"> <p className="text-stone-700 dark:text-stone-300">
</p> </p>) : (
) : (
<> <>
<PageTitle> <PageTitle>
{material.tag {material.tag
+15 -37
ファイルの表示
@@ -25,8 +25,7 @@ import type {
MaterialIndexSort, MaterialIndexSort,
MaterialIndexSuppression, MaterialIndexSuppression,
MaterialIndexTagState, MaterialIndexTagState,
MaterialIndexView, MaterialIndexView } from '@/types'
} from '@/types'
const MEDIA_KIND_LABELS: Record<Material['mediaKind'], string> = { const MEDIA_KIND_LABELS: Record<Material['mediaKind'], string> = {
image: '画像', image: '画像',
@@ -34,8 +33,7 @@ const MEDIA_KIND_LABELS: Record<Material['mediaKind'], string> = {
audio: '音声', audio: '音声',
file_other: 'その他ファイル', file_other: 'その他ファイル',
url_only: 'URL のみ', url_only: 'URL のみ',
suppressed: '抑止済み', suppressed: '抑止済み'}
}
const MEDIA_FILTER_LABELS: Record<MaterialIndexMediaKind, string> = { const MEDIA_FILTER_LABELS: Record<MaterialIndexMediaKind, string> = {
all: 'すべて', all: 'すべて',
@@ -43,8 +41,7 @@ const MEDIA_FILTER_LABELS: Record<MaterialIndexMediaKind, string> = {
video: '動画', video: '動画',
audio: '音声', audio: '音声',
file_other: 'その他ファイル', file_other: 'その他ファイル',
url_only: 'URL のみ', url_only: 'URL のみ'}
}
const SORT_LABELS: Record<MaterialIndexSort, string> = { const SORT_LABELS: Record<MaterialIndexSort, string> = {
created_at: '作成日時', created_at: '作成日時',
@@ -53,8 +50,7 @@ const SORT_LABELS: Record<MaterialIndexSort, string> = {
media_kind: '種類', media_kind: '種類',
file_byte_size: 'ファイルサイズ', file_byte_size: 'ファイルサイズ',
version_no: 'バージョン', version_no: 'バージョン',
id: 'ID', id: 'ID'}
}
const setIf = (qs: URLSearchParams, key: string, value: string | null) => { const setIf = (qs: URLSearchParams, key: string, value: string | null) => {
@@ -67,8 +63,7 @@ const setIf = (qs: URLSearchParams, key: string, value: string | null) => {
const parseOption = <T extends string> ( const parseOption = <T extends string> (
value: string | null, value: string | null,
allowed: readonly T[], allowed: readonly T[],
fallback: T, fallback: T): T => allowed.includes (value as T) ? value as T : fallback
): T => allowed.includes (value as T) ? value as T : fallback
const fileSizeText = (bytes: number | null): string => { const fileSizeText = (bytes: number | null): string => {
@@ -93,12 +88,10 @@ const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
material.fileSuppressedAt material.fileSuppressedAt
? [ ? [
'border-red-300 bg-red-50 text-red-900 dark:border-red-800', 'border-red-300 bg-red-50 text-red-900 dark:border-red-800',
'dark:bg-red-950 dark:text-red-100', 'dark:bg-red-950 dark:text-red-100'].join (' ')
].join (' ')
: [ : [
'border-stone-200 bg-white text-stone-900 dark:border-stone-700', 'border-stone-200 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100', 'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
].join (' ') }`}>
{material.thumbnail {material.thumbnail
? <img src={material.thumbnail} alt="" className="h-full w-full object-contain"/> ? <img src={material.thumbnail} alt="" className="h-full w-full object-contain"/>
: ( : (
@@ -134,12 +127,10 @@ const MaterialListItem: FC<{ material: Material }> = ({ material }) => (
material.fileSuppressedAt material.fileSuppressedAt
? [ ? [
'border-red-200 bg-red-50 text-red-900 dark:border-red-900', 'border-red-200 bg-red-50 text-red-900 dark:border-red-900',
'dark:bg-red-950 dark:text-red-100', 'dark:bg-red-950 dark:text-red-100'].join (' ')
].join (' ')
: [ : [
'border-stone-200 bg-white text-stone-900 dark:border-stone-700', 'border-stone-200 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100', 'dark:bg-stone-900 dark:text-stone-100'].join (' ')}`}>
].join (' ')}`}>
<div className="flex gap-3"> <div className="flex gap-3">
<MaterialThumb material={material}/> <MaterialThumb material={material}/>
<div className="min-w-0 flex-1 space-y-2"> <div className="min-w-0 flex-1 space-y-2">
@@ -239,12 +230,10 @@ const MaterialListPage: FC = () => {
direction, direction,
view, view,
page, page,
limit, limit}
}
const { data, isLoading, isError } = useQuery ({ const { data, isLoading, isError } = useQuery ({
queryKey: materialsKeys.index (keys), queryKey: materialsKeys.index (keys),
queryFn: () => fetchMaterials (keys), queryFn: () => fetchMaterials (keys)})
})
const materials = data?.materials ?? [] const materials = data?.materials ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0 const totalPages = data ? Math.ceil (data.count / limit) : 0
@@ -305,13 +294,6 @@ const MaterialListPage: FC = () => {
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<PageTitle></PageTitle> <PageTitle></PageTitle>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<PrefetchLink
to={`/materials?tag_state=untagged&material_filter=${ materialFilter }`}
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-sm
text-stone-900 hover:bg-stone-100 dark:border-stone-700
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
</PrefetchLink>
<PrefetchLink <PrefetchLink
to="/materials/new" to="/materials/new"
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-sm className="rounded-full border border-stone-300 bg-white px-4 py-2 text-sm
@@ -432,12 +414,10 @@ const MaterialListPage: FC = () => {
view === 'card' view === 'card'
? [ ? [
'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400', 'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
'dark:bg-sky-950 dark:text-sky-100', 'dark:bg-sky-950 dark:text-sky-100'].join (' ')
].join (' ')
: [ : [
'border-stone-300 bg-white text-stone-900 dark:border-stone-700', 'border-stone-300 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100', 'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
].join (' ') }`}>
</button> </button>
<button <button
@@ -447,12 +427,10 @@ const MaterialListPage: FC = () => {
view === 'list' view === 'list'
? [ ? [
'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400', 'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
'dark:bg-sky-950 dark:text-sky-100', 'dark:bg-sky-950 dark:text-sky-100'].join (' ')
].join (' ')
: [ : [
'border-stone-300 bg-white text-stone-900 dark:border-stone-700', 'border-stone-300 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100', 'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
].join (' ') }`}>
</button> </button>
</div> </div>
+1 -2
ファイルの表示
@@ -59,8 +59,7 @@ const MaterialNewPage: FC = () => {
onError: error => { onError: error => {
applyValidationError (error) applyValidationError (error)
toast ({ title: '送信失敗……', description: '入力を見直してください.' }) toast ({ title: '送信失敗……', description: '入力を見直してください.' })
}, }})
})
const handleSubmit = () => { const handleSubmit = () => {
clearValidationErrors () clearValidationErrors ()
+2 -4
ファイルの表示
@@ -62,8 +62,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
toast ({ title: '失敗……', description: '通信に失敗しました……' }) toast ({ title: '失敗……', description: '通信に失敗しました……' })
}, },
onSuccess: () => { onSuccess: () => {
qc.invalidateQueries ({ queryKey: postsKeys.root }) qc.invalidateQueries ({ queryKey: postsKeys.root })} })
} })
useEffect (() => { useEffect (() => {
if (!(errorFlg)) if (!(errorFlg))
@@ -114,8 +113,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
<PostList posts={[{ ...post, childPosts: [{ } as Post] }, <PostList posts={[{ ...post, childPosts: [{ } as Post] },
...post.childPosts!.map (p => ({ ...post.childPosts!.map (p => ({
...p, parentPosts: [{ } as Post] }))]}/> ...p, parentPosts: [{ } as Post] }))]}/>
</div> </div>)}
)}
{(post.parentPosts ?? []).map (pp => { {(post.parentPosts ?? []).map (pp => {
const siblings = post.siblingPosts?.[String (pp.id) as `${ number }`] const siblings = post.siblingPosts?.[String (pp.id) as `${ number }`]
if (!(siblings)) if (!(siblings))
+14 -20
ファイルの表示
@@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, LoaderCircle, Pencil, X } from 'lucide-react' import { Check, LoaderCircle, Pencil, X } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react' import { Fragment, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
@@ -87,8 +87,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
const defaultDirection = { const defaultDirection = {
name: 'asc', name: 'asc',
created_at: 'desc', created_at: 'desc',
updated_at: 'desc', updated_at: 'desc'} as const
} as const
const beginEdit = async (tag: NicoTag) => { const beginEdit = async (tag: NicoTag) => {
const editingTag = nicoTags.find (tag => tag.id === editingId) const editingTag = nicoTags.find (tag => tag.id === editingId)
@@ -99,15 +98,13 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
&& !(await dialogue.confirm ({ && !(await dialogue.confirm ({
title: '編集中の内容を破棄しますか?', title: '編集中の内容を破棄しますか?',
confirmText: '破棄', confirmText: '破棄',
variant: 'danger', variant: 'danger'})))
})))
return return
setEditingId (tag.id) setEditingId (tag.id)
setRawTags (rawTags => ({ setRawTags (rawTags => ({
...rawTags, ...rawTags,
[tag.id]: tag.linkedTags.map (linkedTag => linkedTag.name).join (' '), [tag.id]: tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')}))
}))
setErrorsByTagId (errors => ({ ...errors, [tag.id]: [] })) setErrorsByTagId (errors => ({ ...errors, [tag.id]: [] }))
} }
@@ -115,8 +112,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
setEditingId (null) setEditingId (null)
setRawTags (rawTags => ({ setRawTags (rawTags => ({
...rawTags, ...rawTags,
[tag.id]: tag.linkedTags.map (linkedTag => linkedTag.name).join (' '), [tag.id]: tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')}))
}))
setErrorsByTagId (errors => ({ ...errors, [tag.id]: [] })) setErrorsByTagId (errors => ({ ...errors, [tag.id]: [] }))
} }
@@ -141,8 +137,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
...errors, ...errors,
[id]: validationError?.fieldErrors.tags [id]: validationError?.fieldErrors.tags
?? validationError?.baseErrors ?? validationError?.baseErrors
?? ['更新できませんでした.'], ?? ['更新できませんでした.']}))
}))
toast ({ title: '更新失敗', description: '入力内容を確認してください.' }) toast ({ title: '更新失敗', description: '入力内容を確認してください.' })
} }
finally finally
@@ -166,8 +161,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
setRawTags (Object.fromEntries (data.tags.map (tag => [ setRawTags (Object.fromEntries (data.tags.map (tag => [
tag.id, tag.id,
tag.linkedTags.map (linkedTag => linkedTag.name).join (' '), tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')])))
])))
}, [data]) }, [data])
useEffect (() => { useEffect (() => {
@@ -282,7 +276,8 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
{nicoTags.map (tag => { {nicoTags.map (tag => {
const isEditing = editingId === tag.id const isEditing = editingId === tag.id
return [ return (
<Fragment key={tag.id}>
<tr <tr
key={tag.id} key={tag.id}
className={cn ( className={cn (
@@ -321,8 +316,8 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
</button>)} </button>)}
</td>)} </td>)}
</tr>, </tr>
isEditing && ( {isEditing && (
<tr key={`${ tag.id }-edit`} <tr key={`${ tag.id }-edit`}
className="border-b border-rose-200 bg-rose-50 dark:border-rose-900 className="border-b border-rose-200 bg-rose-50 dark:border-rose-900
dark:bg-rose-950/30"> dark:bg-rose-950/30">
@@ -340,8 +335,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
placeholder="タグ名を空白または改行で区切って入力" placeholder="タグ名を空白または改行で区切って入力"
onChange={e => setRawTags (rawTags => ({ onChange={e => setRawTags (rawTags => ({
...rawTags, ...rawTags,
[tag.id]: e.target.value, [tag.id]: e.target.value}))}/>
}))}/>
<FieldError messages={errorsByTagId[tag.id]}/> <FieldError messages={errorsByTagId[tag.id]}/>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button <button
@@ -368,8 +362,8 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
</div> </div>
</div> </div>
</td> </td>
</tr>), </tr>)}
] </Fragment>)
})} })}
</tbody> </tbody>
</table> </table>
+2 -4
ファイルの表示
@@ -69,8 +69,7 @@ const userName = (user: Pick<User, 'id' | 'name'> | null | undefined): string =>
const commentBox = ( const commentBox = (
comment: TheatreComment, comment: TheatreComment,
programme: TheatreProgramme | null = null, programme: TheatreProgramme | null = null): ReactNode[] =>
): ReactNode[] =>
[( [(
<div key={`${ comment.no }-content`} className="w-full"> <div key={`${ comment.no }-content`} className="w-full">
{comment.deleted {comment.deleted
@@ -120,8 +119,7 @@ const tagsByCategory = (tags: Tag[]): Partial<Record<Category, Tag[]>> => {
const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = ( const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = (
{ tags, compact, flow = 'vertical' }, { tags, compact, flow = 'vertical' }) => {
) => {
const grouped = tagsByCategory (tags) const grouped = tagsByCategory (tags)
if (flow === 'horizontal') if (flow === 'horizontal')
+1 -2
ファイルの表示
@@ -15,5 +15,4 @@ export const useSharedTransitionStore = create<SharedTransitionState> (set => ({
set (state => { set (state => {
const next = { ...state.byLocationKey } const next = { ...state.byLocationKey }
delete next[locationKey] delete next[locationKey]
return { byLocationKey: next } return { byLocationKey: next }}) }))
}) }))