このコミットが含まれているのは:
@@ -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
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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 }) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -10,41 +10,35 @@ 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: {
|
||||||
default:
|
default:
|
||||||
'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-300',
|
'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-300',
|
||||||
|
|
||||||
destructive:
|
destructive:
|
||||||
'bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600',
|
'bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600',
|
||||||
|
|
||||||
outline:
|
outline:
|
||||||
'border border-slate-300 bg-white text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800',
|
'border border-slate-300 bg-white text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800',
|
||||||
|
|
||||||
secondary:
|
secondary:
|
||||||
'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700',
|
'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700',
|
||||||
|
|
||||||
ghost:
|
ghost:
|
||||||
'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>,
|
||||||
@@ -57,13 +51,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
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 }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -29,16 +27,12 @@ const toastVariants = cva(
|
|||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
{
|
{
|
||||||
default: "border bg-background text-foreground",
|
variants: {
|
||||||
destructive:
|
variant: {
|
||||||
"destructive group border-destructive bg-destructive text-destructive-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>,
|
||||||
|
|||||||
@@ -26,6 +26,5 @@ export const Toaster = () => {
|
|||||||
<ToastClose />
|
<ToastClose />
|
||||||
</Toast>))}
|
</Toast>))}
|
||||||
<ToastViewport />
|
<ToastViewport />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -69,17 +68,14 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
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
|
||||||
@@ -87,36 +83,31 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
// but I'll keep it here for simplicity
|
// but I'll keep it here for simplicity
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId)
|
addToRemoveQueue(toastId)
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
state.toasts.forEach((toast) => {
|
||||||
addToRemoveQueue(toast.id)
|
addToRemoveQueue(toast.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
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({
|
||||||
@@ -150,16 +140,13 @@ function toast({ ...props }: Toast) {
|
|||||||
id,
|
id,
|
||||||
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() {
|
||||||
@@ -170,7 +157,7 @@ function useToast() {
|
|||||||
return () => {
|
return () => {
|
||||||
const index = listeners.indexOf(setState)
|
const index = listeners.indexOf(setState)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
listeners.splice(index, 1)
|
listeners.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state])
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
+21
-29
@@ -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,22 +37,20 @@ 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,
|
||||||
media_kind: mediaKind,
|
media_kind: mediaKind,
|
||||||
suppression,
|
suppression,
|
||||||
...(createdFrom && { created_from: createdFrom }),
|
...(createdFrom && { created_from: createdFrom }),
|
||||||
...(createdTo && { created_to: createdTo }),
|
...(createdTo && { created_to: createdTo }),
|
||||||
...(updatedFrom && { updated_from: updatedFrom }),
|
...(updatedFrom && { updated_from: updatedFrom }),
|
||||||
...(updatedTo && { updated_to: updatedTo }),
|
...(updatedTo && { updated_to: updatedTo }),
|
||||||
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,22 +68,19 @@ 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`,
|
||||||
{ params: { material_filter: materialFilter } })
|
{ params: { material_filter: materialFilter } })
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -38,12 +38,11 @@ export const materialsKeys = {
|
|||||||
['materials', 'tag', name, materialFilter] as const,
|
['materials', 'tag', name, materialFilter] as const,
|
||||||
show: (id: string) => ['materials', id] as const,
|
show: (id: string) => ['materials', id] as const,
|
||||||
tree: (p: {
|
tree: (p: {
|
||||||
parentId?: number | null
|
parentId?: number | null
|
||||||
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,
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 } })
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }`
|
||||||
: ''
|
: ''
|
||||||
@@ -60,8 +59,8 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
setExportPath (material.exportPaths.legacyDrive ?? '')
|
setExportPath (material.exportPaths.legacyDrive ?? '')
|
||||||
if (material.file && material.contentType)
|
if (material.file && material.contentType)
|
||||||
{
|
{
|
||||||
setFilePreview (material.file)
|
setFilePreview (material.file)
|
||||||
setFile (null)
|
setFile (null)
|
||||||
}
|
}
|
||||||
}, [material])
|
}, [material])
|
||||||
|
|
||||||
@@ -71,42 +70,40 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
|
|
||||||
const updateMutation = useMutation ({
|
const updateMutation = useMutation ({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const formData = new FormData
|
const formData = new FormData
|
||||||
if (tag.trim ())
|
if (tag.trim ())
|
||||||
formData.append ('tag', tag)
|
formData.append ('tag', tag)
|
||||||
if (file)
|
if (file)
|
||||||
formData.append ('file', file)
|
formData.append ('file', file)
|
||||||
if (url.trim ())
|
if (url.trim ())
|
||||||
formData.append ('url', url)
|
formData.append ('url', url)
|
||||||
formData.append ('export_paths[legacy_drive]', exportPath)
|
formData.append ('export_paths[legacy_drive]', exportPath)
|
||||||
|
|
||||||
return await updateMaterial (id ?? '', formData)
|
return await updateMaterial (id ?? '', formData)
|
||||||
},
|
},
|
||||||
onSuccess: async data => {
|
onSuccess: async data => {
|
||||||
qc.setQueryData (materialsKeys.show (id ?? ''), data)
|
qc.setQueryData (materialsKeys.show (id ?? ''), data)
|
||||||
await invalidateMaterialQueries ()
|
await invalidateMaterialQueries ()
|
||||||
toast ({ title: '更新成功!' })
|
toast ({ title: '更新成功!' })
|
||||||
},
|
},
|
||||||
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) =>
|
||||||
await suppressMaterialFile (id ?? '', { reason }),
|
await suppressMaterialFile (id ?? '', { reason }),
|
||||||
onSuccess: async data => {
|
onSuccess: async data => {
|
||||||
qc.setQueryData (materialsKeys.show (id ?? ''), data)
|
qc.setQueryData (materialsKeys.show (id ?? ''), data)
|
||||||
setFile (null)
|
setFile (null)
|
||||||
setFilePreview ('')
|
setFilePreview ('')
|
||||||
await invalidateMaterialQueries ()
|
await invalidateMaterialQueries ()
|
||||||
toast ({ title: '抑止しました' })
|
toast ({ title: '抑止しました' })
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast ({ title: '抑止に失敗しました' })
|
toast ({ title: '抑止に失敗しました' })
|
||||||
},
|
}})
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
clearValidationErrors ()
|
clearValidationErrors ()
|
||||||
@@ -125,155 +122,153 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
{material && (
|
{material && (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{`${ materialTitle } 素材照会 | ${ SITE_TITLE }`}</title>
|
<title>{`${ materialTitle } 素材照会 | ${ SITE_TITLE }`}</title>
|
||||||
</Helmet>)}
|
</Helmet>)}
|
||||||
|
|
||||||
{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>
|
||||||
<>
|
{material.tag
|
||||||
<PageTitle>
|
? (
|
||||||
{material.tag
|
<TagLink
|
||||||
? (
|
tag={material.tag}
|
||||||
<TagLink
|
withWiki={false}
|
||||||
tag={material.tag}
|
withCount={false}/>)
|
||||||
withWiki={false}
|
: materialTitle}
|
||||||
withCount={false}/>)
|
</PageTitle>
|
||||||
: materialTitle}
|
|
||||||
</PageTitle>
|
|
||||||
|
|
||||||
{material.fileSuppressedAt && (
|
{material.fileSuppressedAt && (
|
||||||
<div className="mb-4 rounded border border-red-300 bg-red-50 p-3
|
<div className="mb-4 rounded border border-red-300 bg-red-50 p-3
|
||||||
text-red-900 dark:border-red-800 dark:bg-red-950
|
text-red-900 dark:border-red-800 dark:bg-red-950
|
||||||
dark:text-red-100">
|
dark:text-red-100">
|
||||||
<span>素材ファイルは抑止済みです。</span>
|
<span>素材ファイルは抑止済みです。</span>
|
||||||
{material.fileSuppressionReason && (
|
{material.fileSuppressionReason && (
|
||||||
<span> 理由: {material.fileSuppressionReason}</span>)}
|
<span> 理由: {material.fileSuppressionReason}</span>)}
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
{(!material.fileSuppressedAt && material.file && material.contentType) && (
|
{(!material.fileSuppressedAt && material.file && material.contentType) && (
|
||||||
(/image\/.*/.test (material.contentType) && (
|
(/image\/.*/.test (material.contentType) && (
|
||||||
<img src={material.file} alt={material.tag?.name || undefined}/>))
|
<img src={material.file} alt={material.tag?.name || undefined}/>))
|
||||||
|| (/video\/.*/.test (material.contentType) && (
|
|| (/video\/.*/.test (material.contentType) && (
|
||||||
<video src={material.file} controls/>))
|
<video src={material.file} controls/>))
|
||||||
|| (/audio\/.*/.test (material.contentType) && (
|
|| (/audio\/.*/.test (material.contentType) && (
|
||||||
<audio src={material.file} controls/>)))}
|
<audio src={material.file} controls/>)))}
|
||||||
|
|
||||||
<TabGroup>
|
<TabGroup>
|
||||||
<Tab name="Wiki">
|
<Tab name="Wiki">
|
||||||
{material.tag
|
{material.tag
|
||||||
? (
|
? (
|
||||||
<WikiBody
|
<WikiBody
|
||||||
title={material.tag.name}
|
title={material.tag.name}
|
||||||
body={material.wikiPageBody ?? undefined}/>)
|
body={material.wikiPageBody ?? undefined}/>)
|
||||||
: (
|
: (
|
||||||
<p className="text-stone-700 dark:text-stone-300">
|
<p className="text-stone-700 dark:text-stone-300">
|
||||||
タグ未設定の素材です.
|
タグ未設定の素材です.
|
||||||
</p>)}
|
</p>)}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab name="編輯">
|
<Tab name="編輯">
|
||||||
<div className="max-w-wl space-y-4 pt-2">
|
<div className="max-w-wl space-y-4 pt-2">
|
||||||
<FieldError messages={baseErrors}/>
|
<FieldError messages={baseErrors}/>
|
||||||
|
|
||||||
<FormField label="タグ" messages={fieldErrors.tag}>
|
<FormField label="タグ" messages={fieldErrors.tag}>
|
||||||
{({ describedBy, invalid }) => (
|
{({ describedBy, invalid }) => (
|
||||||
<TagInput
|
<TagInput
|
||||||
describedBy={describedBy}
|
describedBy={describedBy}
|
||||||
invalid={invalid}
|
invalid={invalid}
|
||||||
value={tag}
|
value={tag}
|
||||||
setValue={setTag}/>)}
|
setValue={setTag}/>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="ファイル" messages={fieldErrors.file}>
|
<FormField label="ファイル" messages={fieldErrors.file}>
|
||||||
{({ describedBy, invalid }) => (
|
{({ describedBy, invalid }) => (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*,video/*,audio/*"
|
accept="image/*,video/*,audio/*"
|
||||||
aria-describedby={describedBy}
|
aria-describedby={describedBy}
|
||||||
aria-invalid={invalid}
|
aria-invalid={invalid}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const nextFile = e.target.files?.[0]
|
const nextFile = e.target.files?.[0]
|
||||||
setFile (nextFile ?? null)
|
setFile (nextFile ?? null)
|
||||||
setFilePreview (
|
setFilePreview (
|
||||||
nextFile ? URL.createObjectURL (nextFile) : '')
|
nextFile ? URL.createObjectURL (nextFile) : '')
|
||||||
}}/>
|
}}/>
|
||||||
{(file && filePreview) && (
|
{(file && filePreview) && (
|
||||||
(/image\/.*/.test (file.type) && (
|
(/image\/.*/.test (file.type) && (
|
||||||
<img
|
<img
|
||||||
src={filePreview}
|
src={filePreview}
|
||||||
alt="preview"
|
alt="preview"
|
||||||
className="mt-2 max-h-48 rounded border"/>))
|
className="mt-2 max-h-48 rounded border"/>))
|
||||||
|| (/video\/.*/.test (file.type) && (
|
|| (/video\/.*/.test (file.type) && (
|
||||||
<video
|
<video
|
||||||
src={filePreview}
|
src={filePreview}
|
||||||
controls
|
controls
|
||||||
className="mt-2 max-h-48 rounded border"/>))
|
className="mt-2 max-h-48 rounded border"/>))
|
||||||
|| (/audio\/.*/.test (file.type) && (
|
|| (/audio\/.*/.test (file.type) && (
|
||||||
<audio
|
<audio
|
||||||
src={filePreview}
|
src={filePreview}
|
||||||
controls
|
controls
|
||||||
className="mt-2 max-h-48"/>))
|
className="mt-2 max-h-48"/>))
|
||||||
|| (
|
|| (
|
||||||
<p className="text-red-600 dark:text-red-400">
|
<p className="text-red-600 dark:text-red-400">
|
||||||
その形式のファイルには対応していません.
|
その形式のファイルには対応していません.
|
||||||
</p>))}
|
</p>))}
|
||||||
</>)}
|
</>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="参考 URL" messages={fieldErrors.url}>
|
<FormField label="参考 URL" messages={fieldErrors.url}>
|
||||||
{({ describedBy, invalid }) => (
|
{({ describedBy, invalid }) => (
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={e => setURL (e.target.value)}
|
onChange={e => setURL (e.target.value)}
|
||||||
aria-describedby={describedBy}
|
aria-describedby={describedBy}
|
||||||
aria-invalid={invalid}
|
aria-invalid={invalid}
|
||||||
className={inputClass (invalid)}/>)}
|
className={inputClass (invalid)}/>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="ZIP 出力パス" messages={fieldErrors.exportPaths}>
|
<FormField label="ZIP 出力パス" messages={fieldErrors.exportPaths}>
|
||||||
{({ describedBy, invalid }) => (
|
{({ describedBy, invalid }) => (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={exportPath}
|
value={exportPath}
|
||||||
onChange={e => setExportPath (e.target.value)}
|
onChange={e => setExportPath (e.target.value)}
|
||||||
placeholder="伊地知ニジカ/表情/泣き.png"
|
placeholder="伊地知ニジカ/表情/泣き.png"
|
||||||
aria-describedby={describedBy}
|
aria-describedby={describedBy}
|
||||||
aria-invalid={invalid}
|
aria-invalid={invalid}
|
||||||
className={inputClass (invalid)}/>)}
|
className={inputClass (invalid)}/>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className="rounded bg-blue-600 px-4 py-2 text-white
|
className="rounded bg-blue-600 px-4 py-2 text-white
|
||||||
disabled:bg-gray-400"
|
disabled:bg-gray-400"
|
||||||
disabled={updateMutation.isPending}>
|
disabled={updateMutation.isPending}>
|
||||||
更新
|
更新
|
||||||
</Button>
|
</Button>
|
||||||
{user?.role === 'admin' && !material.fileSuppressedAt && (
|
{user?.role === 'admin' && !material.fileSuppressedAt && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleSuppress}
|
onClick={handleSuppress}
|
||||||
disabled={suppressMutation.isPending}>
|
disabled={suppressMutation.isPending}>
|
||||||
ファイルを抑止
|
ファイルを抑止
|
||||||
</Button>)}
|
</Button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
</>)}
|
</>)}
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 => {
|
||||||
@@ -88,92 +83,88 @@ const materialTitle = (material: Material): string =>
|
|||||||
|
|
||||||
const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
|
const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
|
||||||
<div
|
<div
|
||||||
className={`flex aspect-square h-[180px] w-[180px] items-center justify-center
|
className={`flex aspect-square h-[180px] w-[180px] items-center justify-center
|
||||||
overflow-hidden rounded-lg border text-center shadow-sm ${
|
overflow-hidden rounded-lg border text-center shadow-sm ${
|
||||||
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'].join (' ') }`}>
|
||||||
'dark:bg-stone-900 dark:text-stone-100',
|
|
||||||
].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"/>
|
||||||
: (
|
: (
|
||||||
<span
|
<span
|
||||||
className="px-2 text-2xl leading-tight"
|
className="px-2 text-2xl leading-tight"
|
||||||
style={{ fontFamily: material.thumbnailFallbackKind === 'tag_name'
|
style={{ fontFamily: material.thumbnailFallbackKind === 'tag_name'
|
||||||
? 'Nikumaru'
|
? 'Nikumaru'
|
||||||
: undefined }}>
|
: undefined }}>
|
||||||
{material.thumbnailFallbackText}
|
{material.thumbnailFallbackText}
|
||||||
</span>)}
|
</span>)}
|
||||||
</div>)
|
</div>)
|
||||||
|
|
||||||
|
|
||||||
const MaterialCard: FC<{ material: Material }> = ({ material }) => (
|
const MaterialCard: FC<{ material: Material }> = ({ material }) => (
|
||||||
<article className="w-[180px] justify-self-center">
|
<article className="w-[180px] justify-self-center">
|
||||||
<PrefetchLink to={`/materials/${ material.id }`} className="block">
|
<PrefetchLink to={`/materials/${ material.id }`} className="block">
|
||||||
<MaterialThumb material={material}/>
|
<MaterialThumb material={material}/>
|
||||||
<div className="mt-2 w-[180px]">
|
<div className="mt-2 w-[180px]">
|
||||||
<p className="truncate text-sm font-medium text-stone-900 dark:text-stone-100">
|
<p className="truncate text-sm font-medium text-stone-900 dark:text-stone-100">
|
||||||
{materialTitle (material)}
|
{materialTitle (material)}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-stone-600 dark:text-stone-300">
|
<p className="truncate text-xs text-stone-600 dark:text-stone-300">
|
||||||
{MEDIA_KIND_LABELS[material.mediaKind]} / {dateString (material.createdAt)}
|
{MEDIA_KIND_LABELS[material.mediaKind]} / {dateString (material.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</PrefetchLink>
|
</PrefetchLink>
|
||||||
</article>)
|
</article>)
|
||||||
|
|
||||||
|
|
||||||
const MaterialListItem: FC<{ material: Material }> = ({ material }) => (
|
const MaterialListItem: FC<{ material: Material }> = ({ material }) => (
|
||||||
<article
|
<article
|
||||||
className={`rounded-lg border p-3 shadow-sm ${
|
className={`rounded-lg border p-3 shadow-sm ${
|
||||||
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'].join (' ')}`}>
|
||||||
'dark:bg-stone-900 dark:text-stone-100',
|
|
||||||
].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">
|
||||||
<div>
|
<div>
|
||||||
<PrefetchLink
|
<PrefetchLink
|
||||||
to={`/materials/${ material.id }`}
|
to={`/materials/${ material.id }`}
|
||||||
className="font-medium text-sky-700 underline underline-offset-2
|
className="font-medium text-sky-700 underline underline-offset-2
|
||||||
dark:text-sky-300">
|
dark:text-sky-300">
|
||||||
{materialTitle (material)}
|
{materialTitle (material)}
|
||||||
</PrefetchLink>
|
</PrefetchLink>
|
||||||
{material.fileSuppressedAt && (
|
{material.fileSuppressedAt && (
|
||||||
<p className="mt-1 text-sm text-red-700 dark:text-red-200">抑止済み</p>)}
|
<p className="mt-1 text-sm text-red-700 dark:text-red-200">抑止済み</p>)}
|
||||||
</div>
|
</div>
|
||||||
<dl className="space-y-1 text-sm text-stone-600 dark:text-stone-300">
|
<dl className="space-y-1 text-sm text-stone-600 dark:text-stone-300">
|
||||||
<div>
|
<div>
|
||||||
<dt className="inline">種類: </dt>
|
<dt className="inline">種類: </dt>
|
||||||
<dd className="inline">{MEDIA_KIND_LABELS[material.mediaKind]}</dd>
|
<dd className="inline">{MEDIA_KIND_LABELS[material.mediaKind]}</dd>
|
||||||
</div>
|
</div>
|
||||||
{material.fileByteSize != null && (
|
{material.fileByteSize != null && (
|
||||||
<div>
|
<div>
|
||||||
<dt className="inline">サイズ: </dt>
|
<dt className="inline">サイズ: </dt>
|
||||||
<dd className="inline">{fileSizeText (material.fileByteSize)}</dd>
|
<dd className="inline">{fileSizeText (material.fileByteSize)}</dd>
|
||||||
</div>)}
|
</div>)}
|
||||||
{material.url && (
|
{material.url && (
|
||||||
<div>
|
<div>
|
||||||
<dt className="inline">URL: </dt>
|
<dt className="inline">URL: </dt>
|
||||||
<dd className="inline break-all">{material.url}</dd>
|
<dd className="inline break-all">{material.url}</dd>
|
||||||
</div>)}
|
</div>)}
|
||||||
<div>
|
<div>
|
||||||
<dt className="inline">作成: </dt>
|
<dt className="inline">作成: </dt>
|
||||||
<dd className="inline">{dateString (material.createdAt)}</dd>
|
<dd className="inline">{dateString (material.createdAt)}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>)
|
</article>)
|
||||||
|
|
||||||
@@ -190,9 +181,9 @@ const MaterialListPage: FC = () => {
|
|||||||
const tagState = query.get ('unclassified') === '1'
|
const tagState = query.get ('unclassified') === '1'
|
||||||
? 'untagged'
|
? 'untagged'
|
||||||
: parseOption<MaterialIndexTagState> (
|
: parseOption<MaterialIndexTagState> (
|
||||||
query.get ('tag_state'),
|
query.get ('tag_state'),
|
||||||
['all', 'tagged', 'untagged'],
|
['all', 'tagged', 'untagged'],
|
||||||
'all')
|
'all')
|
||||||
const mediaKind = parseOption<MaterialIndexMediaKind> (
|
const mediaKind = parseOption<MaterialIndexMediaKind> (
|
||||||
query.get ('media_kind'),
|
query.get ('media_kind'),
|
||||||
['all', 'image', 'video', 'audio', 'file_other', 'url_only'],
|
['all', 'image', 'video', 'audio', 'file_other', 'url_only'],
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -289,214 +278,203 @@ const MaterialListPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
@font-face
|
@font-face
|
||||||
{
|
{
|
||||||
font-family: 'Nikumaru';
|
font-family: 'Nikumaru';
|
||||||
src: url(${ nikumaru }) format('opentype');
|
src: url(${ nikumaru }) format('opentype');
|
||||||
}`}
|
}`}
|
||||||
</style>
|
</style>
|
||||||
<title>{`素材一覧 | ${ SITE_TITLE }`}</title>
|
<title>{`素材一覧 | ${ SITE_TITLE }`}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<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
|
<PrefetchLink
|
||||||
to={`/materials?tag_state=untagged&material_filter=${ materialFilter }`}
|
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
|
||||||
text-stone-900 hover:bg-stone-100 dark:border-stone-700
|
text-stone-900 hover:bg-stone-100 dark:border-stone-700
|
||||||
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
|
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
|
||||||
未分類素材
|
新規素材を追加
|
||||||
</PrefetchLink>
|
</PrefetchLink>
|
||||||
<PrefetchLink
|
<a
|
||||||
to="/materials/new"
|
href={`${ API_BASE_URL }/materials/download.zip?profile=legacy_drive`}
|
||||||
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-sm
|
target="_blank"
|
||||||
text-stone-900 hover:bg-stone-100 dark:border-stone-700
|
rel="noopener noreferrer"
|
||||||
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
|
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
|
||||||
</PrefetchLink>
|
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
|
||||||
<a
|
ZIP をダウンロード
|
||||||
href={`${ API_BASE_URL }/materials/download.zip?profile=legacy_drive`}
|
</a>
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
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">
|
|
||||||
ZIP をダウンロード
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tagState === 'untagged' && (
|
{tagState === 'untagged' && (
|
||||||
<PrefetchLink
|
<PrefetchLink
|
||||||
to={`/materials?material_filter=${ materialFilter }`}
|
to={`/materials?material_filter=${ materialFilter }`}
|
||||||
className="text-sm text-sky-700 underline underline-offset-2 dark:text-sky-300">
|
className="text-sm text-sky-700 underline underline-offset-2 dark:text-sky-300">
|
||||||
素材検索トップへ戻る
|
素材検索トップへ戻る
|
||||||
</PrefetchLink>)}
|
</PrefetchLink>)}
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={search}
|
onSubmit={search}
|
||||||
className="max-w-3xl rounded-lg border border-stone-200 bg-white p-4
|
className="max-w-3xl rounded-lg border border-stone-200 bg-white p-4
|
||||||
text-stone-900 dark:border-stone-700 dark:bg-stone-900
|
text-stone-900 dark:border-stone-700 dark:bg-stone-900
|
||||||
dark:text-stone-100">
|
dark:text-stone-100">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<FormField label="検索">
|
<FormField label="検索">
|
||||||
{({ invalid }) => (
|
{({ invalid }) => (
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
value={q}
|
value={q}
|
||||||
onChange={e => setQ (e.target.value)}
|
onChange={e => setQ (e.target.value)}
|
||||||
placeholder="タグ名 / URL / ファイル名"
|
placeholder="タグ名 / URL / ファイル名"
|
||||||
className={inputClass (invalid)}/>)}
|
className={inputClass (invalid)}/>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="タグ状態">
|
<FormField label="タグ状態">
|
||||||
{({ invalid }) => (
|
{({ invalid }) => (
|
||||||
<select
|
<select
|
||||||
value={tagStateInput}
|
value={tagStateInput}
|
||||||
onChange={e => setTagStateInput (
|
onChange={e => setTagStateInput (
|
||||||
e.target.value as MaterialIndexTagState)}
|
e.target.value as MaterialIndexTagState)}
|
||||||
className={inputClass (invalid)}>
|
className={inputClass (invalid)}>
|
||||||
<option value="all">すべて</option>
|
<option value="all">すべて</option>
|
||||||
<option value="tagged">タグあり</option>
|
<option value="tagged">タグあり</option>
|
||||||
<option value="untagged">タグなし</option>
|
<option value="untagged">タグなし</option>
|
||||||
</select>)}
|
</select>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="メディア">
|
<FormField label="メディア">
|
||||||
{({ invalid }) => (
|
{({ invalid }) => (
|
||||||
<select
|
<select
|
||||||
value={mediaKindInput}
|
value={mediaKindInput}
|
||||||
onChange={e => setMediaKindInput (
|
onChange={e => setMediaKindInput (
|
||||||
e.target.value as MaterialIndexMediaKind)}
|
e.target.value as MaterialIndexMediaKind)}
|
||||||
className={inputClass (invalid)}>
|
className={inputClass (invalid)}>
|
||||||
{Object.entries (MEDIA_FILTER_LABELS).map (([value, label]) => (
|
{Object.entries (MEDIA_FILTER_LABELS).map (([value, label]) => (
|
||||||
<option key={value} value={value}>
|
<option key={value} value={value}>
|
||||||
{label}
|
{label}
|
||||||
</option>))}
|
</option>))}
|
||||||
</select>)}
|
</select>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="抑止状態">
|
<FormField label="抑止状態">
|
||||||
{({ invalid }) => (
|
{({ invalid }) => (
|
||||||
<select
|
<select
|
||||||
value={suppressionInput}
|
value={suppressionInput}
|
||||||
onChange={e => setSuppressionInput (
|
onChange={e => setSuppressionInput (
|
||||||
e.target.value as MaterialIndexSuppression)}
|
e.target.value as MaterialIndexSuppression)}
|
||||||
className={inputClass (invalid)}>
|
className={inputClass (invalid)}>
|
||||||
<option value="active">有効のみ</option>
|
<option value="active">有効のみ</option>
|
||||||
<option value="suppressed">抑止済みのみ</option>
|
<option value="suppressed">抑止済みのみ</option>
|
||||||
<option value="all">すべて</option>
|
<option value="all">すべて</option>
|
||||||
</select>)}
|
</select>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="作成日時">
|
<FormField label="作成日時">
|
||||||
{() => (
|
{() => (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<DateTimeField value={createdFrom ?? undefined} onChange={setCreatedFrom}/>
|
<DateTimeField value={createdFrom ?? undefined} onChange={setCreatedFrom}/>
|
||||||
<span>〜</span>
|
<span>〜</span>
|
||||||
<DateTimeField value={createdTo ?? undefined} onChange={setCreatedTo}/>
|
<DateTimeField value={createdTo ?? undefined} onChange={setCreatedTo}/>
|
||||||
</div>)}
|
</div>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="更新日時">
|
<FormField label="更新日時">
|
||||||
{() => (
|
{() => (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<DateTimeField value={updatedFrom ?? undefined} onChange={setUpdatedFrom}/>
|
<DateTimeField value={updatedFrom ?? undefined} onChange={setUpdatedFrom}/>
|
||||||
<span>〜</span>
|
<span>〜</span>
|
||||||
<DateTimeField value={updatedTo ?? undefined} onChange={setUpdatedTo}/>
|
<DateTimeField value={updatedTo ?? undefined} onChange={setUpdatedTo}/>
|
||||||
</div>)}
|
</div>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-wrap gap-3">
|
<div className="mt-4 flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded bg-sky-600 px-4 py-2 text-white hover:bg-sky-700
|
className="rounded bg-sky-600 px-4 py-2 text-white hover:bg-sky-700
|
||||||
dark:bg-sky-500 dark:text-stone-950 dark:hover:bg-sky-400">
|
dark:bg-sky-500 dark:text-stone-950 dark:hover:bg-sky-400">
|
||||||
検索
|
検索
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateQuery ({ view: 'card' })}
|
onClick={() => updateQuery ({ view: 'card' })}
|
||||||
className={`rounded-full border px-4 py-2 text-sm ${
|
className={`rounded-full border px-4 py-2 text-sm ${
|
||||||
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'].join (' ') }`}>
|
||||||
'dark:bg-stone-900 dark:text-stone-100',
|
カード
|
||||||
].join (' ') }`}>
|
</button>
|
||||||
カード
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
onClick={() => updateQuery ({ view: 'list' })}
|
||||||
type="button"
|
className={`rounded-full border px-4 py-2 text-sm ${
|
||||||
onClick={() => updateQuery ({ view: 'list' })}
|
view === 'list'
|
||||||
className={`rounded-full border px-4 py-2 text-sm ${
|
? [
|
||||||
view === 'list'
|
'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
|
||||||
? [
|
'dark:bg-sky-950 dark:text-sky-100'].join (' ')
|
||||||
'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
|
: [
|
||||||
'dark:bg-sky-950 dark:text-sky-100',
|
'border-stone-300 bg-white text-stone-900 dark:border-stone-700',
|
||||||
].join (' ')
|
'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
|
||||||
: [
|
一覧
|
||||||
'border-stone-300 bg-white text-stone-900 dark:border-stone-700',
|
</button>
|
||||||
'dark:bg-stone-900 dark:text-stone-100',
|
</div>
|
||||||
].join (' ') }`}>
|
|
||||||
一覧
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||||
<select
|
<select
|
||||||
value={sort}
|
value={sort}
|
||||||
onChange={e => updateQuery ({ sort: e.target.value, page: '1' })}
|
onChange={e => updateQuery ({ sort: e.target.value, page: '1' })}
|
||||||
className={inputClass (false, 'w-auto')}>
|
className={inputClass (false, 'w-auto')}>
|
||||||
{Object.entries (SORT_LABELS).map (([value, label]) => (
|
{Object.entries (SORT_LABELS).map (([value, label]) => (
|
||||||
<option key={value} value={value}>
|
<option key={value} value={value}>
|
||||||
{label}
|
{label}
|
||||||
</option>))}
|
</option>))}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={direction}
|
value={direction}
|
||||||
onChange={e => updateQuery ({ direction: e.target.value, page: '1' })}
|
onChange={e => updateQuery ({ direction: e.target.value, page: '1' })}
|
||||||
className={inputClass (false, 'w-auto')}>
|
className={inputClass (false, 'w-auto')}>
|
||||||
<option value="desc">降順</option>
|
<option value="desc">降順</option>
|
||||||
<option value="asc">昇順</option>
|
<option value="asc">昇順</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading && <p>Loading...</p>}
|
{isLoading && <p>Loading...</p>}
|
||||||
{isError && (
|
{isError && (
|
||||||
<p className="text-red-600 dark:text-red-300">素材一覧の取得に失敗しました.</p>)}
|
<p className="text-red-600 dark:text-red-300">素材一覧の取得に失敗しました.</p>)}
|
||||||
{(!isLoading && !isError && materials.length === 0) && (
|
{(!isLoading && !isError && materials.length === 0) && (
|
||||||
<p>素材はありません.</p>)}
|
<p>素材はありません.</p>)}
|
||||||
{materials.length > 0 && (
|
{materials.length > 0 && (
|
||||||
view === 'card'
|
view === 'card'
|
||||||
? (
|
? (
|
||||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(196px,1fr))]
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(196px,1fr))]
|
||||||
justify-items-center gap-4">
|
justify-items-center gap-4">
|
||||||
{materials.map (material => (
|
{materials.map (material => (
|
||||||
<MaterialCard key={material.id} material={material}/>))}
|
<MaterialCard key={material.id} material={material}/>))}
|
||||||
</div>)
|
</div>)
|
||||||
: (
|
: (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{materials.map (material => (
|
{materials.map (material => (
|
||||||
<MaterialListItem key={material.id} material={material}/>))}
|
<MaterialListItem key={material.id} material={material}/>))}
|
||||||
</div>))}
|
</div>))}
|
||||||
<Pagination page={page} totalPages={totalPages}/>
|
<Pagination page={page} totalPages={totalPages}/>
|
||||||
</div>
|
</div>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,27 +40,26 @@ const MaterialNewPage: FC = () => {
|
|||||||
|
|
||||||
const createMutation = useMutation ({
|
const createMutation = useMutation ({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const formData = new FormData
|
const formData = new FormData
|
||||||
if (tag)
|
if (tag)
|
||||||
formData.append ('tag', tag)
|
formData.append ('tag', tag)
|
||||||
if (file)
|
if (file)
|
||||||
formData.append ('file', file)
|
formData.append ('file', file)
|
||||||
if (url)
|
if (url)
|
||||||
formData.append ('url', url)
|
formData.append ('url', url)
|
||||||
formData.append ('export_paths[legacy_drive]', exportPath)
|
formData.append ('export_paths[legacy_drive]', exportPath)
|
||||||
|
|
||||||
return await createMaterial (formData)
|
return await createMaterial (formData)
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
|
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
|
||||||
toast ({ title: '送信成功!' })
|
toast ({ title: '送信成功!' })
|
||||||
navigate (`/materials?tag=${ encodeURIComponent (tag) }`)
|
navigate (`/materials?tag=${ encodeURIComponent (tag) }`)
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
applyValidationError (error)
|
applyValidationError (error)
|
||||||
toast ({ title: '送信失敗……', description: '入力を見直してください.' })
|
toast ({ title: '送信失敗……', description: '入力を見直してください.' })
|
||||||
},
|
}})
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
clearValidationErrors ()
|
clearValidationErrors ()
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 }}) }))
|
||||||
}) }))
|
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする