From c10ba7a698eecad7acd0fc95275e03a6a2809263 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 25 Jun 2026 08:11:41 +0900 Subject: [PATCH] #306 --- AGENTS.md | 13 + frontend/src/components/MaterialSidebar.tsx | 37 +- frontend/src/components/NicoViewer.tsx | 3 +- .../PostOriginalCreatedTimeField.tsx | 3 +- frontend/src/components/TagDetailSidebar.tsx | 15 +- frontend/src/components/common/Pagination.tsx | 3 +- .../components/dialogues/DialogueProvider.tsx | 3 +- frontend/src/components/ui/button.tsx | 54 +- frontend/src/components/ui/dialog.tsx | 30 +- frontend/src/components/ui/input.tsx | 41 +- frontend/src/components/ui/switch.tsx | 55 +- frontend/src/components/ui/toast.tsx | 244 ++++---- frontend/src/components/ui/toaster.tsx | 61 +- frontend/src/components/ui/use-toast.tsx | 70 +-- frontend/src/config.sample.ts | 3 +- frontend/src/consts.ts | 15 +- frontend/src/lib/api.ts | 18 +- frontend/src/lib/gekanator.ts | 42 +- .../src/lib/gekanatorCandidateRecovery.ts | 12 +- frontend/src/lib/gekanatorQuestionFilters.ts | 24 +- frontend/src/lib/materials.ts | 50 +- frontend/src/lib/posts.ts | 9 +- frontend/src/lib/queryKeys.ts | 7 +- frontend/src/lib/tags.ts | 12 +- frontend/src/lib/users.ts | 3 +- frontend/src/lib/utils.ts | 6 +- frontend/src/lib/wiki.ts | 9 +- frontend/src/pages/GekanatorPage.tsx | 219 +++---- .../deerjikists/DeerjikistDetailPage.tsx | 6 +- .../pages/materials/MaterialDetailPage.tsx | 331 ++++++----- .../src/pages/materials/MaterialListPage.tsx | 540 +++++++++--------- .../src/pages/materials/MaterialNewPage.tsx | 31 +- frontend/src/pages/posts/PostDetailPage.tsx | 6 +- frontend/src/pages/tags/NicoTagListPage.tsx | 34 +- .../src/pages/theatres/TheatreDetailPage.tsx | 6 +- frontend/src/stores/sharedTransitionStore.ts | 3 +- 36 files changed, 886 insertions(+), 1132 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6dfdee8..908fc0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -268,7 +268,20 @@ const value = - Put two blank lines before and after top-level `const` function declarations, unless imports, exports, or file boundaries make that awkward. - 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. +- 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 `{{...props}}`. - Keep a tag's closing marker on the same line as the final prop when the tag spans multiple lines. - Do not put `/>` or `>` on its own line unless the existing surrounding code diff --git a/frontend/src/components/MaterialSidebar.tsx b/frontend/src/components/MaterialSidebar.tsx index 39b066d..13c6562 100644 --- a/frontend/src/components/MaterialSidebar.tsx +++ b/frontend/src/components/MaterialSidebar.tsx @@ -2,7 +2,6 @@ import { Fragment, useEffect, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useLocation, useNavigate } from 'react-router-dom' -import PrefetchLink from '@/components/PrefetchLink' import TagLink from '@/components/TagLink' import SidebarComponent from '@/components/layout/SidebarComponent' import { materialsKeys } from '@/lib/queryKeys' @@ -17,15 +16,13 @@ const FILTERS: MaterialFilter[] = ['present', 'missing', 'any'] const FILTER_LABELS: Record = { present: '素材あり', missing: '素材なし', - any: 'すべて', -} + any: 'すべて'} const setChildrenById = ( tags: MaterialSidebarTag[], targetId: number, - children: MaterialSidebarTag[], -): MaterialSidebarTag[] => ( + children: MaterialSidebarTag[]): MaterialSidebarTag[] => ( tags.map (tag => { if (tag.id === targetId) return { ...tag, children } @@ -39,8 +36,7 @@ const setChildrenById = ( const materialPath = ( tagName: string, - materialFilter: MaterialFilter, -): string => `/materials?q=${ encodeURIComponent (tagName) }&material_filter=${ materialFilter }` + materialFilter: MaterialFilter): string => `/materials?q=${ encodeURIComponent (tagName) }&material_filter=${ materialFilter }` const sidebarTagToTag = (tag: MaterialSidebarTag): Tag => ({ @@ -63,8 +59,7 @@ const updateMaterialFilterQuery = ( pathname: string, locationSearch: string, navigate: ReturnType, - materialFilter: MaterialFilter, -) => { + materialFilter: MaterialFilter) => { const qs = new URLSearchParams (locationSearch) qs.set ('material_filter', materialFilter) navigate (`${ pathname }${ qs.toString () ? `?${ qs.toString () }` : '' }`) @@ -104,8 +99,7 @@ const MaterialTreeNode: FC<{ const { data } = useQuery ({ queryKey: materialsKeys.tree ({ 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 (() => { if (open && data && tag.children.length === 0) @@ -164,8 +158,7 @@ const MobileMaterialTreeNode: FC<{ const { data } = useQuery ({ queryKey: materialsKeys.tree ({ 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 (() => { if (open && data && tag.children.length === 0) @@ -233,8 +226,7 @@ const MaterialSidebar: FC = () => { const { data: rootTags = [], isLoading, isError } = useQuery ({ queryKey: materialsKeys.tree ({ parentId: null, materialFilter }), - queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter }), - }) + queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter })}) useEffect (() => { setDesktopTags (rootTags) @@ -279,14 +271,6 @@ const MaterialSidebar: FC = () => { <>
-
- タグ一覧 - - 未分類素材 - -
@@ -310,13 +294,6 @@ const MaterialSidebar: FC = () => { -
- - 未分類素材 - -
{isLoading && (

読込中……

)} {isError && ( diff --git a/frontend/src/components/NicoViewer.tsx b/frontend/src/components/NicoViewer.tsx index 97f1ffb..061c5db 100644 --- a/frontend/src/components/NicoViewer.tsx +++ b/frontend/src/components/NicoViewer.tsx @@ -107,8 +107,7 @@ export default forwardRef ((props: Props, ref: ForwardedRef { onError?.({ eventName: 'loadCompleteTimeout', - reason: 'niconico video length was not reported by embed', - }) + reason: 'niconico video length was not reported by embed'}) }, LOAD_COMPLETE_TIMEOUT_MS) }, [clearLoadCompleteTimer, onError]) diff --git a/frontend/src/components/PostOriginalCreatedTimeField.tsx b/frontend/src/components/PostOriginalCreatedTimeField.tsx index 2ddbee4..863e344 100644 --- a/frontend/src/components/PostOriginalCreatedTimeField.tsx +++ b/frontend/src/components/PostOriginalCreatedTimeField.tsx @@ -19,8 +19,7 @@ const PostOriginalCreatedTimeField: FC = ( setOriginalCreatedFrom, originalCreatedBefore, setOriginalCreatedBefore, - errors }: Props, -) => ( + errors }: Props) => ( {({ describedBy, invalid }) => ( <> diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index be95412..64b45bb 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -37,8 +37,7 @@ const renderTagTree = ( path: string, suppressClickRef: MutableRefObject, parentTagId?: number, - sp?: boolean, -): ReactNode[] => { + sp?: boolean): ReactNode[] => { const key = `${ path }-${ tag.id }` const self = ( @@ -64,8 +63,7 @@ const renderTagTree = ( const isDescendant = ( root: Tag, - targetId: number, -): boolean => { + targetId: number): boolean => { if (!(root.children)) return false @@ -83,8 +81,7 @@ const isDescendant = ( const findTag = ( byCat: TagByCategory, - id: number, -): Tag | undefined => { + id: number): Tag | undefined => { const walk = (nodes: Tag[]): Tag | undefined => { for (const t of nodes) { @@ -130,8 +127,7 @@ const buildTagByCategory = (post: Post): TagByCategory => { const changeCategory = async ( tagId: number, - category: Category, -): Promise => { + category: Category): Promise => { await apiPatch (`/tags/${ tagId }`, { category }) } @@ -294,8 +290,7 @@ const TagDetailSidebar: FC = ({ post, sp }) => { addEventListener ('click', e => { e.preventDefault () e.stopPropagation () - suppressClickRef.current = false - }, { capture: true, once: true }) + suppressClickRef.current = false}, { capture: true, once: true }) }} onDragCancel={() => { setActiveTagId (null) diff --git a/frontend/src/components/common/Pagination.tsx b/frontend/src/components/common/Pagination.tsx index 798d079..763b77c 100644 --- a/frontend/src/components/common/Pagination.tsx +++ b/frontend/src/components/common/Pagination.tsx @@ -16,8 +16,7 @@ const range = (start: number, end: number): number[] => const getPages = ( page: number, total: number, - siblingCount: number, -): (number | '…')[] => { + siblingCount: number): (number | '…')[] => { if (total <= 1) return [1] diff --git a/frontend/src/components/dialogues/DialogueProvider.tsx b/frontend/src/components/dialogues/DialogueProvider.tsx index 669518f..fb77ecf 100644 --- a/frontend/src/components/dialogues/DialogueProvider.tsx +++ b/frontend/src/components/dialogues/DialogueProvider.tsx @@ -103,8 +103,7 @@ const DialogueProvider: FC = ({ children }) => { choice: options => new Promise (resolve => { push ({ kind: 'choice', options: options as ChoiceOptions, - resolve: resolve as (value: string | null) => void }) - }) }), [push]) + resolve: resolve as (value: string | null) => void })}) }), [push]) const active = queue[0] diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 34abd10..49b4a45 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -10,41 +10,35 @@ const buttonVariants = cva ( 'rounded-md text-sm font-medium transition-colors', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400', 'disabled:pointer-events-none disabled:opacity-50', - '[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', - ].join (' '), + '[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'].join (' '), { variants: { variant: { - default: - 'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-300', + default: + 'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-300', - destructive: - 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600', + destructive: + 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600', - 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', + 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', - secondary: - 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700', + secondary: + 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700', - ghost: - 'text-slate-900 hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-800', + ghost: + 'text-slate-900 hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-800', - link: - 'text-blue-700 underline-offset-4 hover:underline dark:text-blue-300', - }, + link: + 'text-blue-700 underline-offset-4 hover:underline dark:text-blue-300'}, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', - }, - }, + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10'}}, defaultVariants: { variant: 'default', - size: 'default', - }, - }) + size: 'default'}}) export interface ButtonProps extends React.ButtonHTMLAttributes, @@ -57,13 +51,11 @@ const Button = React.forwardRef( const Comp = asChild ? Slot : "button" return ( - ) - } -) + className={cn(buttonVariants({ variant, size, className }))} + ref={ref} + {...props} + />) + }) Button.displayName = "Button" export { Button, buttonVariants } diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index 0eb3096..a6e9a8e 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -22,11 +22,9 @@ const DialogOverlay = React.forwardRef< ref={ref} 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", - className - )} + className)} {...props} - /> -)) + />)) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< @@ -62,8 +60,7 @@ const DialogContent = React.forwardRef< 閉ぢる - -)) + )) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ @@ -73,11 +70,9 @@ const DialogHeader = ({
-) + />) DialogHeader.displayName = "DialogHeader" const DialogFooter = ({ @@ -87,11 +82,9 @@ const DialogFooter = ({
-) + />) DialogFooter.displayName = "DialogFooter" const DialogTitle = React.forwardRef< @@ -102,11 +95,9 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className - )} + className)} {...props} - /> -)) + />)) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< @@ -117,8 +108,7 @@ const DialogDescription = React.forwardRef< ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} - /> -)) + />)) DialogDescription.displayName = DialogPrimitive.Description.displayName export { diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 68551b9..1ba33a5 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -1,22 +1,19 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + ) + } ) +Input.displayName = "Input" + +export { Input } diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx index bc69cf2..49127ff 100644 --- a/frontend/src/components/ui/switch.tsx +++ b/frontend/src/components/ui/switch.tsx @@ -1,29 +1,26 @@ -"use client" - -import * as React from "react" -import * as SwitchPrimitives from "@radix-ui/react-switch" - -import { cn } from "@/lib/utils" - -const Switch = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -Switch.displayName = SwitchPrimitives.Root.displayName - -export { Switch } +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + )) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx index 521b94b..01b546d 100644 --- a/frontend/src/components/ui/toast.tsx +++ b/frontend/src/components/ui/toast.tsx @@ -1,129 +1,115 @@ -"use client" - -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -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", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + )) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +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", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { variant: "default" } } ) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + ) }) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + )) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + )) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + )) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + )) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/frontend/src/components/ui/toaster.tsx b/frontend/src/components/ui/toaster.tsx index b6147e4..dc8cfee 100644 --- a/frontend/src/components/ui/toaster.tsx +++ b/frontend/src/components/ui/toaster.tsx @@ -1,31 +1,30 @@ -'use client' - -import { useToast } from '@/components/ui/use-toast' -import { Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport } from '@/components/ui/toast' - - -export const Toaster = () => { - const { toasts } = useToast () - - return ( - - {toasts.map (({ id, title, description, action, ...props }) => ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
))} - -
- ) -} +'use client' + +import { useToast } from '@/components/ui/use-toast' +import { Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport } from '@/components/ui/toast' + + +export const Toaster = () => { + const { toasts } = useToast () + + return ( + + {toasts.map (({ id, title, description, action, ...props }) => ( + +
+ {title && {title}} + {description && {description}} +
+ {action} + +
))} + +
) +} diff --git a/frontend/src/components/ui/use-toast.tsx b/frontend/src/components/ui/use-toast.tsx index e8e7155..dcfbd7d 100644 --- a/frontend/src/components/ui/use-toast.tsx +++ b/frontend/src/components/ui/use-toast.tsx @@ -58,8 +58,7 @@ const addToRemoveQueue = (toastId: string) => { toastTimeouts.delete(toastId) dispatch({ type: "REMOVE_TOAST", - toastId: toastId, - }) + toastId: toastId}) }, TOAST_REMOVE_DELAY) toastTimeouts.set(toastId, timeout) @@ -69,17 +68,14 @@ export const reducer = (state: State, action: Action): State => { switch (action.type) { case "ADD_TOAST": return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)} case "UPDATE_TOAST": return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t)} case "DISMISS_TOAST": { 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, // but I'll keep it here for simplicity if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId) } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) } return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false} + : t)} } case "REMOVE_TOAST": if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } + return { + ...state, + toasts: []} } return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId)} } } @@ -139,8 +130,7 @@ function toast({ ...props }: Toast) { const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", - toast: { ...props, id }, - }) + toast: { ...props, id }}) const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) dispatch({ @@ -150,16 +140,13 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) + if (!open) dismiss() + }}}) return { id: id, dismiss, - update, - } + update} } function useToast() { @@ -170,7 +157,7 @@ function useToast() { return () => { const index = listeners.indexOf(setState) if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1) } } }, [state]) @@ -178,8 +165,7 @@ function useToast() { return { ...state, toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId })} } export { useToast, toast } diff --git a/frontend/src/config.sample.ts b/frontend/src/config.sample.ts index 620811c..dc9e1eb 100644 --- a/frontend/src/config.sample.ts +++ b/frontend/src/config.sample.ts @@ -3,8 +3,7 @@ const ENV: string = 'development' const config = { 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 SITE_TITLE = config.SITE_TITLE diff --git a/frontend/src/consts.ts b/frontend/src/consts.ts index 466b7f0..a7be4f4 100644 --- a/frontend/src/consts.ts +++ b/frontend/src/consts.ts @@ -10,8 +10,7 @@ export const CATEGORIES = [ 'general', 'material', 'meta', - 'nico', - ] as const + 'nico'] as const export const CATEGORY_NAMES: Record = { deerjikist: 'ニジラー', @@ -20,16 +19,14 @@ export const CATEGORY_NAMES: Record = { general: '一般', material: '素材', meta: 'メタタグ', - nico: 'ニコニコタグ', - } as const + nico: 'ニコニコタグ'} as const export const FETCH_POSTS_ORDER_FIELDS = [ 'title', 'url', 'original_created_at', 'created_at', - 'updated_at', - ] as const + 'updated_at'] as const export const PLATFORMS = ['nico', 'youtube'] as const @@ -43,13 +40,11 @@ export const TAG_COLOUR = { general: 'cyan', material: 'orange', meta: 'yellow', - nico: 'gray', - } as const satisfies Record + nico: 'gray'} as const satisfies Record export const USER_ROLES = ['admin', 'member', 'guest'] as const export const ViewFlagBehavior = { OnShowedDetail: 1, OnClickedLink: 2, - NotAuto: 3, - } as const + NotAuto: 3} as const diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index a93de11..654c51f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -23,8 +23,7 @@ const apiP = async ( method: 'post' | 'put' | 'patch', path: string, body?: unknown, - opt?: Opt, -): Promise => { + opt?: Opt): Promise => { const res = await client[method] (path, body ?? { }, withUserCode (opt)) if (opt?.responseType === 'blob') return res.data as T @@ -34,8 +33,7 @@ const apiP = async ( export const apiGet = async ( path: string, - opt?: Opt, -): Promise => { + opt?: Opt): Promise => { const res = await client.get (path, withUserCode (opt)) if (opt?.responseType === 'blob') return res.data as T @@ -46,28 +44,24 @@ export const apiGet = async ( export const apiPost = async ( path: string, body?: unknown, - opt?: Opt, -): Promise => apiP ('post', path, body, opt) + opt?: Opt): Promise => apiP ('post', path, body, opt) export const apiPut = async ( path: string, body?: unknown, - opt?: Opt, -): Promise => apiP ('put', path, body, opt) + opt?: Opt): Promise => apiP ('put', path, body, opt) export const apiPatch = async ( path: string, body?: unknown, - opt?: Opt, -): Promise => apiP ('patch', path, body, opt) + opt?: Opt): Promise => apiP ('patch', path, body, opt) export const apiDelete = async ( path: string, - opt?: Opt, -): Promise => { + opt?: Opt): Promise => { const res = await client.delete (path, withUserCode (opt)) if (res.data == null || res.data === '') return undefined as T diff --git a/frontend/src/lib/gekanator.ts b/frontend/src/lib/gekanator.ts index 1299b0c..0acb338 100644 --- a/frontend/src/lib/gekanator.ts +++ b/frontend/src/lib/gekanator.ts @@ -104,8 +104,7 @@ export type BuildGekanatorQuestionsOptions = { export const normalizeTitleLengthCondition = ( - condition: GekanatorQuestionCondition, -): GekanatorQuestionCondition => { + condition: GekanatorQuestionCondition): GekanatorQuestionCondition => { switch (condition.type) { case 'title-length-greater-than': @@ -119,8 +118,7 @@ export const normalizeTitleLengthCondition = ( export const titleLengthMinimumForCondition = ( - condition: GekanatorQuestionCondition, -): number | null => { + condition: GekanatorQuestionCondition): number | null => { switch (condition.type) { case 'title-length-at-least': @@ -134,8 +132,7 @@ export const titleLengthMinimumForCondition = ( export const questionIdForCondition = ( - condition: NonPostSimilarityCondition, -): string => { + condition: NonPostSimilarityCondition): string => { switch (condition.type) { case 'tag': @@ -161,8 +158,7 @@ export const questionIdForCondition = ( const directExampleAnswerFor = ( question: StoredGekanatorQuestion, - post: Post, -): GekanatorAnswerValue | null => { + post: Post): GekanatorAnswerValue | null => { if (question.kind !== 'post_similarity' && question.kind !== 'tag') return null @@ -178,15 +174,13 @@ const directExampleAnswerFor = ( export const isLearnedSemanticQuestion = ( - question: StoredGekanatorQuestion | GekanatorQuestion, -): boolean => + question: StoredGekanatorQuestion | GekanatorQuestion): boolean => question.kind === 'post_similarity' && question.source === 'user_suggested' export const learnedSemanticSideForAnswer = ( - answer: GekanatorAnswerValue | null, -): LearnedSemanticSide => { + answer: GekanatorAnswerValue | null): LearnedSemanticSide => { if (answer === 'yes' || answer === 'partial') return 'positive' @@ -314,8 +308,7 @@ const questionableTag = (post: Post, key: string): boolean => { const questionMatches = ( post: Post, - question: StoredGekanatorQuestion, -): boolean => { + question: StoredGekanatorQuestion): boolean => { const directAnswer = directExampleAnswerFor (question, post) if (directAnswer) return question.kind === 'post_similarity' @@ -350,8 +343,7 @@ const questionMatches = ( export const expectedAnswerForQuestion = ( question: StoredGekanatorQuestion | GekanatorQuestion | undefined, - post: Post | null, -): GekanatorAnswerValue | null => { + post: Post | null): GekanatorAnswerValue | null => { if (!(question) || !(post)) return null @@ -382,14 +374,12 @@ export const expectedAnswerForQuestion = ( export const learnedSemanticSideForPost = ( question: StoredGekanatorQuestion | GekanatorQuestion | undefined, - post: Post | null, -): LearnedSemanticSide => + post: Post | null): LearnedSemanticSide => learnedSemanticSideForAnswer (expectedAnswerForQuestion (question, post)) export const restoreGekanatorQuestion = ( - question: StoredGekanatorQuestion, -): GekanatorQuestion => { + question: StoredGekanatorQuestion): GekanatorQuestion => { const normalizedCondition = normalizeTitleLengthCondition (question.condition) const normalizedQuestion = { ...question, @@ -408,8 +398,7 @@ export const restoreGekanatorQuestion = ( export const storeGekanatorQuestion = ( - question: GekanatorQuestion, -): StoredGekanatorQuestion => ({ + question: GekanatorQuestion): StoredGekanatorQuestion => ({ id: question.condition.type === 'title-length-greater-than' ? `title:length-at-least:${ question.condition.length + 1 }` : question.id, @@ -436,8 +425,7 @@ export const fetchGekanatorQuestions = async (): Promise => { + nonce?: string): Promise => { const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> ( `/gekanator/games/${ gameId }/extra_questions`, { params: nonce ? { nonce } : undefined }) @@ -447,8 +435,7 @@ export const fetchGekanatorExtraQuestions = async ( export const buildGekanatorQuestions = ( posts: Post[], - options: BuildGekanatorQuestionsOptions = { }, -): GekanatorQuestion[] => { + options: BuildGekanatorQuestionsOptions = { }): GekanatorQuestion[] => { const { includeTitleContains = true, tagQuestionCap = 192, @@ -490,8 +477,7 @@ export const buildGekanatorQuestions = ( const usefulEntries = ( counts: Map, - cap: number, - ) => + cap: number) => [...counts.entries ()] .filter (([, count]) => count > 0 && count < posts.length) .sort ((a, b) => Math.abs (posts.length / 2 - a[1]) diff --git a/frontend/src/lib/gekanatorCandidateRecovery.ts b/frontend/src/lib/gekanatorCandidateRecovery.ts index aec3123..2c81874 100644 --- a/frontend/src/lib/gekanatorCandidateRecovery.ts +++ b/frontend/src/lib/gekanatorCandidateRecovery.ts @@ -32,8 +32,7 @@ export const candidatePostsFor = ( answers: GekanatorAnswerLog[] softenedQuestionIds: Set rejectedPostIds: Set - recoveredCandidatePosts: Map }, -): Post[] => { + recoveredCandidatePosts: Map }): Post[] => { const questionById = new Map (questions.map (question => [question.id, question])) return posts.filter (post => { @@ -76,8 +75,7 @@ export const candidatePostsFor = ( export const hardFilteredPostsForAnswer = ( { posts, question, answer }: { posts: Post[] question: GekanatorQuestion - answer: GekanatorAnswerValue }, -): Post[] => { + answer: GekanatorAnswerValue }): Post[] => { if (!(questionSupportsAnswerBasedHardFiltering (question))) return posts @@ -98,8 +96,7 @@ const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', ' export const allConcreteAnswerOptionsExhausted = ( posts: Post[], - question: GekanatorQuestion | null, -): boolean => { + question: GekanatorQuestion | null): boolean => { if (!(question)) return false @@ -125,8 +122,7 @@ export const recoverCandidatePosts = ( recoveredCandidatePosts: Map eligiblePostIds: Set answerCountAtRecovery: number - recoveryStepCount: number }, -): { recoveredCandidatePosts: Map + recoveryStepCount: number }): { recoveredCandidatePosts: Map recoveryStepCount: number } | null => { const recovered = new Map (recoveredCandidatePosts) const targetSize = nextRecoveryTargetSize (recoveryStepCount) diff --git a/frontend/src/lib/gekanatorQuestionFilters.ts b/frontend/src/lib/gekanatorQuestionFilters.ts index e358e38..08e6d63 100644 --- a/frontend/src/lib/gekanatorQuestionFilters.ts +++ b/frontend/src/lib/gekanatorQuestionFilters.ts @@ -8,8 +8,7 @@ import type { export const monthForCondition = ( - condition: GekanatorQuestion['condition'], -): number | null => { + condition: GekanatorQuestion['condition']): number | null => { if (condition.type === 'original-month') return condition.month @@ -24,8 +23,7 @@ export const monthForCondition = ( const isTitleLengthContradiction = ( candidate: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'], - answer: GekanatorAnswerValue, -): boolean => { + answer: GekanatorAnswerValue): boolean => { const candidateLength = titleLengthMinimumForCondition (candidate) const previousLength = titleLengthMinimumForCondition (previous) if (candidateLength === null || previousLength === null) @@ -45,8 +43,7 @@ const isTitleLengthContradiction = ( const isQuestionRedundantAfterAnswers = ( question: GekanatorQuestion, - answers: GekanatorAnswerLog[], -): boolean => answers.some (answer => { + answers: GekanatorAnswerLog[]): boolean => answers.some (answer => { const previous = answer.questionCondition return previous !== undefined && isTitleLengthContradiction (question.condition, previous, answer.answer) @@ -56,8 +53,7 @@ const isQuestionRedundantAfterAnswers = ( const isSourceFactBlocked = ( candidate: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'], - answer: GekanatorAnswerValue, -): boolean => { + answer: GekanatorAnswerValue): boolean => { if (candidate.type !== 'source' || previous.type !== 'source') return false @@ -76,8 +72,7 @@ const isSourceFactBlocked = ( const isOriginalYearFactBlocked = ( candidate: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'], - answer: GekanatorAnswerValue, -): boolean => { + answer: GekanatorAnswerValue): boolean => { if (candidate.type !== 'original-year' || previous.type !== 'original-year') return false @@ -96,8 +91,7 @@ const isOriginalYearFactBlocked = ( const isOriginalMonthFactBlocked = ( candidate: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'], - answer: GekanatorAnswerValue, -): boolean => { + answer: GekanatorAnswerValue): boolean => { switch (answer) { case 'yes': @@ -143,8 +137,7 @@ const isOriginalMonthFactBlocked = ( const isFactQuestionBlocked = ( candidate: GekanatorQuestion['condition'], previous: GekanatorQuestion['condition'], - answer: GekanatorAnswerValue, -): boolean => { + answer: GekanatorAnswerValue): boolean => { if (!(answer === 'yes' || answer === 'no')) return false @@ -156,8 +149,7 @@ const isFactQuestionBlocked = ( export const isQuestionHardFilteredAfterAnswers = ( question: GekanatorQuestion, - answers: GekanatorAnswerLog[], -): boolean => answers.some (answer => { + answers: GekanatorAnswerLog[]): boolean => answers.some (answer => { const previous = answer.questionCondition if (previous === undefined) return false diff --git a/frontend/src/lib/materials.ts b/frontend/src/lib/materials.ts index 5e2b570..b0ccf7c 100644 --- a/frontend/src/lib/materials.ts +++ b/frontend/src/lib/materials.ts @@ -29,8 +29,7 @@ const MATERIAL_FILTERS: MaterialFilter[] = ['present', 'missing', 'any'] export const parseMaterialFilter = ( value: unknown, - fallback: MaterialFilter = 'present', -): MaterialFilter => + fallback: MaterialFilter = 'present'): MaterialFilter => typeof value === 'string' && MATERIAL_FILTERS.includes (value as MaterialFilter) ? value as MaterialFilter : fallback @@ -38,22 +37,20 @@ export const parseMaterialFilter = ( export const fetchMaterials = async ( { q, tagState, mediaKind, suppression, createdFrom, createdTo, - updatedFrom, updatedTo, sort, direction, page, limit }: FetchMaterialsParams, -): Promise => + updatedFrom, updatedTo, sort, direction, page, limit }: FetchMaterialsParams): Promise => await apiGet ('/materials', { params: { - ...(q && { q }), - tag_state: tagState, - media_kind: mediaKind, - suppression, - ...(createdFrom && { created_from: createdFrom }), - ...(createdTo && { created_to: createdTo }), - ...(updatedFrom && { updated_from: updatedFrom }), - ...(updatedTo && { updated_to: updatedTo }), - sort, - direction, - page, - limit, - } }) + ...(q && { q }), + tag_state: tagState, + media_kind: mediaKind, + suppression, + ...(createdFrom && { created_from: createdFrom }), + ...(createdTo && { created_to: createdTo }), + ...(updatedFrom && { updated_from: updatedFrom }), + ...(updatedTo && { updated_to: updatedTo }), + sort, + direction, + page, + limit} }) export const fetchMaterial = async (id: string): Promise => { @@ -71,22 +68,19 @@ export const fetchMaterial = async (id: string): Promise => { export const fetchMaterialTagTree = async ( - { parentId, materialFilter }: FetchMaterialTreeParams, -): Promise => + { parentId, materialFilter }: FetchMaterialTreeParams): Promise => await apiGet ('/tags/with-depth', { params: { - ...(parentId != null && { parent: String (parentId) }), - material_filter: materialFilter, - } }) + ...(parentId != null && { parent: String (parentId) }), + material_filter: materialFilter} }) export const fetchMaterialTagByName = async ( name: string, - materialFilter: MaterialFilter, -): Promise => { + materialFilter: MaterialFilter): Promise => { try { return await apiGet (`/tags/name/${ encodeURIComponent (name) }/materials`, - { params: { material_filter: materialFilter } }) + { params: { material_filter: materialFilter } }) } catch (error) { @@ -103,13 +97,11 @@ export const createMaterial = async (formData: FormData): Promise => export const updateMaterial = async ( id: string, - formData: FormData, -): Promise => + formData: FormData): Promise => await apiPut (`/materials/${ id }`, formData) export const suppressMaterialFile = async ( id: string, - payload: { reason: string; purge?: boolean }, -): Promise => + payload: { reason: string; purge?: boolean }): Promise => await apiPatch (`/materials/${ id }/suppress_file`, payload) diff --git a/frontend/src/lib/posts.ts b/frontend/src/lib/posts.ts index a1febff..c192b52 100644 --- a/frontend/src/lib/posts.ts +++ b/frontend/src/lib/posts.ts @@ -5,8 +5,7 @@ import type { FetchPostsParams, Post, PostVersion } from '@/types' export const fetchPosts = async ( { url, title, tags, match, createdFrom, createdTo, updatedFrom, updatedTo, - originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams, -): Promise<{ + originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams): Promise<{ posts: Post[] count: number }> => await apiGet ('/posts', { params: { @@ -33,8 +32,7 @@ export const fetchPostChanges = async ( post?: string tag?: string page: number - limit: number }, -): Promise<{ + limit: number }): Promise<{ versions: PostVersion[] count: number }> => await apiGet ('/posts/versions', { params: { ...(post && { post }), @@ -52,8 +50,7 @@ export const updatePost = async ( { baseVersionNo, force, merge }: { baseVersionNo?: number force?: boolean - merge?: boolean } -) => + merge?: boolean }) => await apiPut ( `/posts/${ post.id }`, { title: post.title, diff --git a/frontend/src/lib/queryKeys.ts b/frontend/src/lib/queryKeys.ts index 3649838..ffa3b86 100644 --- a/frontend/src/lib/queryKeys.ts +++ b/frontend/src/lib/queryKeys.ts @@ -38,12 +38,11 @@ export const materialsKeys = { ['materials', 'tag', name, materialFilter] as const, show: (id: string) => ['materials', id] as const, tree: (p: { - parentId?: number | null - materialFilter: MaterialFilter + parentId?: number | null + materialFilter: MaterialFilter }) => ['materials', 'tree', p] as const, unclassified: (p: { page?: number; limit?: number } = { }) => - ['materials', 'unclassified', p] as const, -} + ['materials', 'unclassified', p] as const} export const wikiKeys = { root: ['wiki'] as const, diff --git a/frontend/src/lib/tags.ts b/frontend/src/lib/tags.ts index 31cad85..4c017ab 100644 --- a/frontend/src/lib/tags.ts +++ b/frontend/src/lib/tags.ts @@ -11,8 +11,7 @@ import type { Deerjikist, export const fetchTags = async ( { post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo, updatedFrom, updatedTo, deprecated, - page, limit, order }: FetchTagsParams, -): Promise<{ tags: Tag[] + page, limit, order }: FetchTagsParams): Promise<{ tags: Tag[] count: number }> => await apiGet ('/tags', { params: { ...(post != null && { post }), @@ -31,8 +30,7 @@ export const fetchTags = async ( export const fetchNicoTags = async ( - { name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams, -): Promise<{ tags: NicoTag[] + { name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams): Promise<{ tags: NicoTag[] count: number }> => await apiGet ('/tags/nico', { params: { page, @@ -70,14 +68,12 @@ export const fetchTagChanges = async ( { id, page, limit }: { id?: string page: number - limit: number }, -): Promise<{ + limit: number }): Promise<{ versions: TagVersion[] count: number }> => await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } }) export const fetchDeerjikistsByTag = async ( - id: string, -): Promise<{ tag: Tag; deerjikists: Deerjikist[]}> => + id: string): Promise<{ tag: Tag; deerjikists: Deerjikist[] }> => await apiGet (`/tags/${ id }/deerjikists`) diff --git a/frontend/src/lib/users.ts b/frontend/src/lib/users.ts index 9f31329..ea5b28c 100644 --- a/frontend/src/lib/users.ts +++ b/frontend/src/lib/users.ts @@ -4,5 +4,4 @@ const CONTENT_EDITOR_ROLES: readonly UserRole[] = ['admin', 'member'] export const canEditContent = ( - user: Pick | null | undefined, -): boolean => user != null && CONTENT_EDITOR_ROLES.includes (user.role) + user: Pick | null | undefined): boolean => user != null && CONTENT_EDITOR_ROLES.includes (user.role) diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 69ccbb8..6e11803 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -12,8 +12,7 @@ export const cn = (...inputs: ClassValue[]) => twMerge (clsx (...inputs)) export const dateString = ( d: string | Date, - unknown: 'month' | 'day' | 'hour' | 'minute' | 'second' | null = null, -): string => + unknown: 'month' | 'day' | 'hour' | 'minute' | 'second' | null = null): string => toDate (d).toLocaleString ( 'ja-JP-u-ca-japanese', { era: 'long', @@ -28,8 +27,7 @@ export const dateString = ( export const originalCreatedAtString = ( f: string | Date | null, - b: string | Date | null, -): string => { + b: string | Date | null): string => { const from = f ? toDate (f) : null const before = b ? toDate (b) : null diff --git a/frontend/src/lib/wiki.ts b/frontend/src/lib/wiki.ts index 276db3a..b6f63c2 100644 --- a/frontend/src/lib/wiki.ts +++ b/frontend/src/lib/wiki.ts @@ -4,22 +4,19 @@ import type { WikiPage } from '@/types' export const fetchWikiPages = async ( - { title }: { title?: string }, -): Promise => + { title }: { title?: string }): Promise => await apiGet ('/wiki', { params: { title } }) export const fetchWikiPage = async ( id: string, - { version }: { version?: string }, -): Promise => + { version }: { version?: string }): Promise => await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } }) export const fetchWikiPageByTitle = async ( title: string, - { version }: { version?: string }, -): Promise => { + { version }: { version?: string }): Promise => { try { return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } }) diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 1eabcd0..6ea1f84 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -238,8 +238,7 @@ const createGameSeed = (): string => { const normalizeStoredQuestionId = ( questionId: string, - condition?: GekanatorQuestionCondition, -): string => { + condition?: GekanatorQuestionCondition): string => { if (condition?.type === 'title-length-greater-than') return `title:length-at-least:${ condition.length + 1 }` @@ -312,8 +311,7 @@ const sourcePriorityForMerge = (question: GekanatorQuestion): number => { const shouldReplaceMergedQuestion = ( current: GekanatorQuestion | undefined, - candidate: GekanatorQuestion, -): boolean => { + candidate: GekanatorQuestion): boolean => { if (!(current)) return true @@ -408,8 +406,7 @@ const loadRecentGames = (): RecentGameSummary[] => { const storeRecentGameSummary = ( - summary: RecentGameSummary, -): RecentGameSummary[] => { + summary: RecentGameSummary): RecentGameSummary[] => { const next = [summary, ...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt @@ -459,8 +456,7 @@ const resettableExtraQuestionState = (): { const recoveredCandidateMapFromStored = ( items: RecoveredCandidatePost[], - scores: [number, number][], -): Map => { + scores: [number, number][]): Map => { const storedScores = new Map (scores) return new Map (items.map (item => [item.postId, { @@ -470,8 +466,7 @@ const recoveredCandidateMapFromStored = ( const storedRecoveredCandidatesFromMap = ( - recoveredCandidatePosts: Map, -): RecoveredCandidatePost[] => + recoveredCandidatePosts: Map): RecoveredCandidatePost[] => [...recoveredCandidatePosts.entries ()] .map (([postId, recoveredCandidate]) => ({ postId, @@ -513,8 +508,7 @@ const distributionEntropy = (weights: number[]): number => const questionCategoryPenalty = ( question: GekanatorQuestion, answerCount: number, - repeatPenalty: number, -): number => { + repeatPenalty: number): number => { const earlyFactor = Math.max (0, (3 - answerCount) / 3) const titleLengthPenalty = (() => { if (titleLengthMinimumForCondition (question.condition) == null) @@ -553,8 +547,7 @@ const relatedPostIdsOf = (post: Post): number[] => { const userPriorWeightsFor = ( posts: Post[], - recentGames: RecentGameSummary[], -): Map => { + recentGames: RecentGameSummary[]): Map => { const postById = new Map (posts.map (post => [post.id, post])) const weights = new Map () const addWeight = (postId: number, weight: number) => { @@ -581,14 +574,12 @@ const userPriorWeightsFor = ( const answerWeightFor = ( questionId: string, - softenedQuestionIds: Set, -): number => softenedQuestionIds.has (questionId) ? softenedAnswerWeight : 1 + softenedQuestionIds: Set): number => softenedQuestionIds.has (questionId) ? softenedAnswerWeight : 1 const scoreWeightForAnswer = ( answer: GekanatorAnswerLog, - softenedQuestionIds: Set, -): number => + softenedQuestionIds: Set): number => answerWeightFor (answer.questionId, softenedQuestionIds) * ( answer.questionPurpose === 'learning_user_suggested' @@ -638,8 +629,7 @@ const titleTermPattern = const addPostIdToIndex = ( index: Map>, key: K, - postId: number, -) => { + postId: number) => { const current = index.get (key) if (current) { @@ -652,8 +642,7 @@ const addPostIdToIndex = ( const buildMaterialIndex = ( - posts: Post[], -): GekanatorQuestionMaterialIndex => { + posts: Post[]): GekanatorQuestionMaterialIndex => { const postById = new Map () const tagKeysByPostId = new Map () const postIdsByTagKey = new Map> () @@ -784,8 +773,7 @@ const originalDateQuestionTextFor = ( condition: Extract< GekanatorQuestionCondition, { type: 'original-year' | 'original-month' | 'original-month-day' } - >, -): string => { + >): string => { switch (condition.type) { case 'original-year': @@ -852,8 +840,7 @@ const isLearnableTagKey = (key: string): boolean => !(key.startsWith ('nico:')) const isUserSuggestedLearnedSemanticQuestion = ( - question: GekanatorQuestion, -): boolean => isLearnedSemanticQuestion (question) + question: GekanatorQuestion): boolean => isLearnedSemanticQuestion (question) type LearnedSemanticCandidateStats = { @@ -872,8 +859,7 @@ const learnedSemanticStatsForCandidateIds = ( question }: { candidateIds: number[] posts: Post[] - question: GekanatorQuestion }, -): LearnedSemanticCandidateStats => { + question: GekanatorQuestion }): LearnedSemanticCandidateStats => { const candidateIdSet = new Set (candidateIds) const positiveIds = new Set () const negativeIds = new Set () @@ -915,8 +901,7 @@ const learnedSemanticQuestionIsEffectiveForCandidateIds = ( question }: { candidateIds: number[] posts: Post[] - question: GekanatorQuestion }, -): boolean => { + question: GekanatorQuestion }): boolean => { if (!(isUserSuggestedLearnedSemanticQuestion (question))) return false @@ -936,8 +921,7 @@ const learnedSemanticQuestionIsEffectiveForCandidateIds = ( const directSemanticAnswerForPost = ( question: GekanatorQuestion, - post: Post, -): GekanatorAnswerValue | null => { + post: Post): GekanatorAnswerValue | null => { const direct = question.exampleAnswers?.[String (post.id) as `${ number }`] if (direct) return direct @@ -956,8 +940,7 @@ const learnedSemanticLearningValueForTopPosts = ( question: GekanatorQuestion learningTargetPosts: Post[] candidateIds: number[] - posts: Post[] }, -): { missingTopCount: number + posts: Post[] }): { missingTopCount: number knownCount: number hasLearningValue: boolean } => { const missingTopCount = @@ -1000,8 +983,7 @@ const learningTargetPostsForCandidates = ({ const questionPurposeCountsFor = ( - answers: GekanatorAnswerLog[], -): { + answers: GekanatorAnswerLog[]): { effectiveUserSuggestedCount: number learningUserSuggestedCount: number normalQuestionCount: number @@ -1041,8 +1023,7 @@ const questionPurposeCountsFor = ( const learnedSemanticNarrowPenaltyForStats = ( candidateCount: number, - stats: LearnedSemanticCandidateStats, -): number => { + stats: LearnedSemanticCandidateStats): number => { const minSide = candidateCount < 10 ? 1 : Math.max (3, candidateCount * .08) return stats.positiveCount < minSide || stats.negativeCount < minSide ? .15 : 0 } @@ -1050,8 +1031,7 @@ const learnedSemanticNarrowPenaltyForStats = ( const learnedSemanticScoreDeltaForExpectedAnswer = ( userAnswer: GekanatorAnswerValue, - expectedAnswer: GekanatorAnswerValue | null, -): number => { + expectedAnswer: GekanatorAnswerValue | null): number => { switch (userAnswer) { case 'yes': @@ -1089,8 +1069,7 @@ const learnedSemanticScoreDeltaForExpectedAnswer = ( const scoreDropDeltaForRecoveredPost = ( postId: number, totalScore: number, - recoveredCandidatePosts: Map, -): number => { + recoveredCandidatePosts: Map): number => { const recoveredCandidate = recoveredCandidatePosts.get (postId) if (recoveredCandidate == null) return totalScore @@ -1114,8 +1093,7 @@ const postPassesScoreDrop = ( recoveredCandidatePosts }: { postId: number scores: Map - recoveredCandidatePosts: Map }, -): boolean => { + recoveredCandidatePosts: Map }): boolean => { if (!(activeCandidateScoreDropEnabled (scores))) return true @@ -1129,8 +1107,7 @@ const postPassesScoreDrop = ( // `post_similarities` is the score-propagation graph, not the question kind. const questionUsesPostSimilarityPropagationGraphForScoring = ( - question: GekanatorQuestion, -): boolean => + question: GekanatorQuestion): boolean => (question.kind === 'post_similarity' && !(isUserSuggestedLearnedSemanticQuestion (question))) || (question.kind === 'tag' @@ -1139,8 +1116,7 @@ const questionUsesPostSimilarityPropagationGraphForScoring = ( const questionSupportsAnswerBasedHardFiltering = ( - question: GekanatorQuestion, -): boolean => !(questionUsesPostSimilarityPropagationGraphForScoring (question)) + question: GekanatorQuestion): boolean => !(questionUsesPostSimilarityPropagationGraphForScoring (question)) && !(isUserSuggestedLearnedSemanticQuestion (question)) @@ -1160,8 +1136,7 @@ const usesLearnedTagExamples = (question: GekanatorQuestion): boolean => const searchedQuestionsFor = ( questions: GekanatorQuestion[], - search: string, -): GekanatorQuestion[] => { + search: string): GekanatorQuestion[] => { const needle = search.trim () if (!(needle)) return [] @@ -1235,8 +1210,7 @@ type QuestionMatchResolver = { const buildGekanatorMatchIndex = ( posts: Post[], - questions: GekanatorQuestion[], -): GekanatorMatchIndex => new Map ( + questions: GekanatorQuestion[]): GekanatorMatchIndex => new Map ( questions.map (question => [ question.id, new Set ( @@ -1285,8 +1259,7 @@ const matchingPostIdsForQuestion = ({ const positiveMatchingPostIdsForQuestion = ( - resolver: QuestionMatchResolver, -): Set => { + resolver: QuestionMatchResolver): Set => { if (isUserSuggestedLearnedSemanticQuestion (resolver.question)) { const cached = resolver.dynamicMatchIndex?.get (resolver.question.id) @@ -1356,8 +1329,7 @@ const matchingWeightInCandidates = ( materialIndex: GekanatorQuestionMaterialIndex matchIndex: GekanatorMatchIndex question: GekanatorQuestion - dynamicMatchIndex?: GekanatorMatchIndex }, -): number => { + dynamicMatchIndex?: GekanatorMatchIndex }): number => { const matched = positiveMatchingPostIdsForQuestion ({ posts, materialIndex, @@ -1380,8 +1352,7 @@ const signatureForCandidateIds = ( materialIndex: GekanatorQuestionMaterialIndex matchIndex: GekanatorMatchIndex question: GekanatorQuestion - dynamicMatchIndex?: GekanatorMatchIndex }, -): string => { + dynamicMatchIndex?: GekanatorMatchIndex }): string => { if (isUserSuggestedLearnedSemanticQuestion (question)) { const postById = new Map (posts.map (post => [post.id, post])) @@ -1421,8 +1392,7 @@ const postIdsForHardAnswer = ( posts: Post[] materialIndex: GekanatorQuestionMaterialIndex matchIndex: GekanatorMatchIndex - dynamicMatchIndex?: GekanatorMatchIndex }, -): number[] => { + dynamicMatchIndex?: GekanatorMatchIndex }): number[] => { if (!(questionSupportsAnswerBasedHardFiltering (question))) return candidateIds @@ -1562,8 +1532,7 @@ const buildIndexedQuestion = ( text: string kind: GekanatorQuestionKind priorityWeight: number - materialIndex: GekanatorQuestionMaterialIndex }, -): GekanatorQuestion => ({ + materialIndex: GekanatorQuestionMaterialIndex }): GekanatorQuestion => ({ id: questionIdForCondition (condition), text, kind, @@ -1578,8 +1547,7 @@ const buildIndexedQuestion = ( const rankedEntriesForCounts = ( { counts, total, cap }: { counts: Map total: number - cap: number }, -): [T, number][] => + cap: number }): [T, number][] => ([...counts.entries ()] .filter (([, count]) => count > 0 && count < total) .sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1])) @@ -1594,8 +1562,7 @@ const buildQuestionsForCandidateIds = ( materialIndex: GekanatorQuestionMaterialIndex acceptedQuestions: GekanatorQuestion[] mode?: QuestionBuildMode - confirmationPostId?: number | null }, -): GekanatorQuestion[] => { + confirmationPostId?: number | null }): GekanatorQuestion[] => { const total = candidateIds.length const confirmationPost = (() => { if (confirmationPostId == null) @@ -1652,8 +1619,7 @@ const buildQuestionsForCandidateIds = ( condition: Extract< GekanatorQuestionCondition, { type: 'original-year' | 'original-month' | 'original-month-day' } - >, - ): GekanatorQuestion => { + >): GekanatorQuestion => { const priorityWeight = (() => { if (condition.type === 'original-year') return 1.04 @@ -1673,8 +1639,7 @@ const buildQuestionsForCandidateIds = ( const specialMonthDays = rankedEntriesForCounts ({ counts: monthDayCounts, total, - cap: factCap - }).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null) + cap: factCap}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null) if (mode === 'split') { @@ -2124,8 +2089,7 @@ type ExclusiveConditionGroup = const exclusiveConditionGroupFor = ( - condition: GekanatorQuestion['condition'], -): ExclusiveConditionGroup | null => { + condition: GekanatorQuestion['condition']): ExclusiveConditionGroup | null => { switch (condition.type) { case 'original-month': @@ -2144,8 +2108,7 @@ const exclusiveConditionGroupFor = ( const sameConditionValue = ( left: GekanatorQuestion['condition'], - right: GekanatorQuestion['condition'], -): boolean => { + right: GekanatorQuestion['condition']): boolean => { const leftTitleLength = titleLengthMinimumForCondition (left) const rightTitleLength = titleLengthMinimumForCondition (right) if (leftTitleLength != null || rightTitleLength != null) @@ -2187,8 +2150,7 @@ const sameConditionValue = ( const isMonthCrossMatch = ( candidate: GekanatorQuestion['condition'], - previous: GekanatorQuestion['condition'], -): boolean => { + previous: GekanatorQuestion['condition']): boolean => { const candidateMonth = monthForCondition (candidate) const previousMonth = monthForCondition (previous) if (candidateMonth == null || previousMonth == null) @@ -2204,8 +2166,7 @@ const isMonthCrossMatch = ( const isExclusiveContradiction = ( candidate: GekanatorQuestion['condition'], - previous: GekanatorQuestion['condition'], -): boolean => { + previous: GekanatorQuestion['condition']): boolean => { const candidateGroup = exclusiveConditionGroupFor (candidate) const previousGroup = exclusiveConditionGroupFor (previous) @@ -2242,16 +2203,14 @@ const contradictionPenaltyFor = ({ case 'no': if ( sameConditionValue (question.condition, previous) - || isMonthCrossMatch (question.condition, previous) - ) + || isMonthCrossMatch (question.condition, previous)) return sum + 40 return sum case 'probably_no': if ( sameConditionValue (question.condition, previous) - || isMonthCrossMatch (question.condition, previous) - ) + || isMonthCrossMatch (question.condition, previous)) return sum + 20 return sum @@ -2281,8 +2240,7 @@ const chooseQuestion = ( recentFirstQuestionPenaltyById: Map userPriorWeights: Map materialIndex: GekanatorQuestionMaterialIndex - matchIndex: GekanatorMatchIndex }, -): QuestionSelection | null => { + matchIndex: GekanatorMatchIndex }): QuestionSelection | null => { const dynamicMatchIndex = new Map> () const invertedSignature = (signature: string): string => @@ -2327,8 +2285,7 @@ const chooseQuestion = ( const rank = ( questionsToRank: GekanatorQuestion[], 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 candidateById = new Map (candidates.map (item => [item.post.id, item.post])) const candidateIds = candidates.map (item => item.post.id) @@ -2613,8 +2570,7 @@ const chooseQuestion = ( if ( effectiveRatio < targetEffectiveUserSuggestedQuestionRatio && totalUserSuggestedRatio < targetTotalUserSuggestedQuestionRatio - && effectiveUserSuggestedPool.length > 0 - ) + && effectiveUserSuggestedPool.length > 0) { selectedPool = effectiveUserSuggestedPool selectedPurpose = 'effective_user_suggested' @@ -2622,8 +2578,7 @@ const chooseQuestion = ( else if ( learningRatio < targetLearningUserSuggestedQuestionRatio && totalUserSuggestedRatio < targetTotalUserSuggestedQuestionRatio - && learningUserSuggestedPool.length > 0 - ) + && learningUserSuggestedPool.length > 0) { selectedPool = learningUserSuggestedPool selectedPurpose = 'learning_user_suggested' @@ -2683,8 +2638,7 @@ const chooseQuestion = ( const winningRunPriorityFor = ( - expected: GekanatorAnswerValue, -): number | null => { + expected: GekanatorAnswerValue): number | null => { if (expected === 'yes') return 0 if (expected === 'partial') @@ -2719,8 +2673,7 @@ const chooseWinningRunQuestion = ({ materialIndex, acceptedQuestions, mode: 'confirmation', - confirmationPostId: targetPost.id - }) + confirmationPostId: targetPost.id}) .filter (question => { if (askedIds.has (question.id)) return false @@ -2829,8 +2782,7 @@ const chooseFallbackQuestion = ({ materialIndex, acceptedQuestions: [], mode: 'confirmation', - confirmationPostId: post.id - }))) + confirmationPostId: post.id}))) .slice (0, 32) const dynamicMatchIndex = new Map> () const ranked = mergeQuestions ([ @@ -2905,8 +2857,7 @@ const chooseFallbackQuestion = ({ const shouldEnterGuessPhase = ( - reason: GuessReason | null, -): reason is 'hard_max_questions' | 'winning_run_finished' | 'question_count_checkpoint' => + reason: GuessReason | null): reason is 'hard_max_questions' | 'winning_run_finished' | 'question_count_checkpoint' => (reason === 'hard_max_questions' || reason === 'winning_run_finished' || reason === 'question_count_checkpoint') @@ -2914,15 +2865,13 @@ const shouldEnterGuessPhase = ( const isWinningRunActive = ( winningRunTargetId: number | null, - winningRunStartAnswerCount: number | null, -): boolean => + winningRunStartAnswerCount: number | null): boolean => winningRunTargetId != null && winningRunStartAnswerCount != null const winningRunQuestionCount = ( answers: GekanatorAnswerLog[], - winningRunStartAnswerCount: number | null, -): number => { + winningRunStartAnswerCount: number | null): number => { if (winningRunStartAnswerCount == null) return 0 @@ -2962,8 +2911,7 @@ const nextQuestionPlanFor = ( matchIndex: GekanatorMatchIndex lastGuessQuestionCount: number winningRunTargetId: number | null - winningRunStartAnswerCount: number | null }, -): { question: GekanatorQuestion | null + winningRunStartAnswerCount: number | null }): { question: GekanatorQuestion | null guess: Post | null guessReason: GuessReason | null questionMode: QuestionMode @@ -3013,8 +2961,7 @@ const nextQuestionPlanFor = ( if ( isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount) && winningRunTargetId === nextWinningRunTargetId - && winningRunStartAnswerCount != null - ) + && winningRunStartAnswerCount != null) return winningRunStartAnswerCount return answers.length @@ -3188,8 +3135,7 @@ const mascotStateFor = ( resultWon: boolean | null, eligiblePostCount: number, bestConfidencePercent: number, - winningRunActive: boolean, -): MascotState => { + winningRunActive: boolean): MascotState => { const resultPhase = phase === 'end' || phase === 'review' @@ -3210,13 +3156,11 @@ const mascotStateFor = ( if ( winningRunActive || eligiblePostCount <= 2 - || bestConfidencePercent >= 70 - ) + || bestConfidencePercent >= 70) return 'thinking_near' if ( eligiblePostCount >= 15 - && bestConfidencePercent < 45 - ) + && bestConfidencePercent < 45) return 'thinking_far' return 'thinking_mid' case 'guess': @@ -3327,8 +3271,7 @@ const GekanatorBackdrop: FC<{ const settingsForMode = useCallback ( ( - mode: 'normal' | 'winning_run' | 'guess', - ): { columns: number; rows: number; opacity: number } => { + mode: 'normal' | 'winning_run' | 'guess'): { columns: number; rows: number; opacity: number } => { if (mode === 'winning_run' || mode === 'guess') return { columns: 8, rows: 8, opacity: motionMode === 'calm' ? .18 : .24 } @@ -3342,8 +3285,7 @@ const GekanatorBackdrop: FC<{ const scaleForMode = useCallback ( ( mode: 'normal' | 'winning_run' | 'guess', - displayedWinningCount: number, - ): number => { + displayedWinningCount: number): number => { if (mode === 'guess') return 8 @@ -3355,8 +3297,7 @@ const GekanatorBackdrop: FC<{ []) const postsForMode = useCallback (( - mode: 'normal' | 'winning_run' | 'guess', - ): Post[] => { + mode: 'normal' | 'winning_run' | 'guess'): Post[] => { if (mode === 'guess' && displayedGuess) return [displayedGuess] if (mode === 'winning_run' && winningRunTargetPost) @@ -3366,8 +3307,7 @@ const GekanatorBackdrop: FC<{ const thumbnailsForMode = useCallback (( mode: 'normal' | 'winning_run' | 'guess', - count: number, - ): string[] => { + count: number): string[] => { const modePosts = postsForMode (mode) if (modePosts.length === 0) return [] @@ -3734,8 +3674,7 @@ const GekanatorBackdrop: FC<{ const expectedAnswerFor = ( question: GekanatorQuestion | undefined, - correctPost: Post | null, -): GekanatorAnswerValue | null => + correctPost: Post | null): GekanatorAnswerValue | null => expectedAnswerForQuestion (question, correctPost) @@ -4169,8 +4108,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setSaved (true) setSavedGameId (data.id) setLearnedExampleCount (data.learnedExampleCount) - setResultWon (variables.guessedPostId === variables.correctPostId) - }}) + setResultWon (variables.guessedPostId === variables.correctPostId)}}) const questionSuggestionMutation = useMutation ({ mutationFn: saveGekanatorQuestionSuggestion, onSuccess: async data => { @@ -4180,15 +4118,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setQuestionSuggestionSearch ('') setQuestionSuggestionSelectedId (null) setQuestionSuggestion ('') - setQuestionSuggestionAnswer ('yes') - }}) + setQuestionSuggestionAnswer ('yes')}}) const extraQuestionAnswersMutation = useMutation ({ mutationFn: saveGekanatorExtraQuestionAnswers, onSuccess: async () => { await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () }) setExtraQuestionState ('saved') - setPhase ('end') - }}) + setPhase ('end')}}) const resetExtraQuestionState = () => { const next = resettableExtraQuestionState () @@ -4330,8 +4266,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { if ( !(allowPreQuestionRecovery) || recoveredEligiblePosts.length === 0 - || recoveredEligiblePosts.length === 1 - ) + || recoveredEligiblePosts.length === 1) return false const nextQuestion = chooseQuestion ({ @@ -4493,8 +4428,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { if ( !(nextPlan.question) && !(shouldEnterGuessPhase (nextPlan.guessReason)) - && recovered.eligiblePosts.length !== 1 - ) + && recovered.eligiblePosts.length !== 1) { const recoveredForQuestion = recoverQuestionState ({ nextAnswers, @@ -4602,8 +4536,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { !(canPersistGame) || reviewGuessedPostId == null || reviewCorrectPostId == null - || saveMutation.isPending - ) + || saveMutation.isPending) return if (savedGameId != null) @@ -4666,8 +4599,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { !(canPersistGame) || savedGameId == null || extraQuestionAnswersMutation.isPending - || extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)])) - ) + || extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)]))) return extraQuestionAnswersMutation.mutate ({ @@ -4870,8 +4802,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const answerExtraQuestion = ( questionId: number, - value: GekanatorAnswerValue, - ) => { + value: GekanatorAnswerValue) => { setExtraQuestionAnswers ({ ...extraQuestionAnswers, [String (questionId)]: value }) @@ -4909,8 +4840,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { || isLoading || acceptedQuestionsLoading || shouldEnterGuessPhase (questionPlan.guessReason) - || eligiblePosts.length === 1 - ) + || eligiblePosts.length === 1) return const recovered = recoverQuestionState ({ @@ -4926,8 +4856,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { if ( recovered.recoveryStepCount === recoveryStepCount && recovered.recoveredCandidatePosts.size === recoveredCandidatePosts.size - && recovered.softenedQuestionIds.size === softenedQuestionIds.size - ) + && recovered.softenedQuestionIds.size === softenedQuestionIds.size) return setSoftenedQuestionIds (recovered.softenedQuestionIds) @@ -4954,15 +4883,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { if ( phase !== 'question' || isLoading - || acceptedQuestionsLoading - ) + || acceptedQuestionsLoading) return if ( currentQuestion || !(questionPlan.guess) - || !(shouldEnterGuessPhase (questionPlan.guessReason)) - ) + || !(shouldEnterGuessPhase (questionPlan.guessReason))) return setWinningRunTargetId (questionPlan.winningRunTargetId) diff --git a/frontend/src/pages/deerjikists/DeerjikistDetailPage.tsx b/frontend/src/pages/deerjikists/DeerjikistDetailPage.tsx index 99dd7ad..e3daec4 100644 --- a/frontend/src/pages/deerjikists/DeerjikistDetailPage.tsx +++ b/frontend/src/pages/deerjikists/DeerjikistDetailPage.tsx @@ -145,8 +145,7 @@ const DeerjikistDetailPage: FC = () => { return rtn })}/>)} - - ))} + ))}
-
- )} +
)} ) } diff --git a/frontend/src/pages/materials/MaterialDetailPage.tsx b/frontend/src/pages/materials/MaterialDetailPage.tsx index 523962a..69fe2aa 100644 --- a/frontend/src/pages/materials/MaterialDetailPage.tsx +++ b/frontend/src/pages/materials/MaterialDetailPage.tsx @@ -45,8 +45,7 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => { const { data: material, isError, isLoading } = useQuery ({ queryKey: materialsKeys.show (id ?? ''), queryFn: () => fetchMaterial (id ?? ''), - enabled: id != null, - }) + enabled: id != null}) const materialTitle = material ? material.tag?.name ?? `素材 #${ material.id }` : '' @@ -60,8 +59,8 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => { setExportPath (material.exportPaths.legacyDrive ?? '') if (material.file && material.contentType) { - setFilePreview (material.file) - setFile (null) + setFilePreview (material.file) + setFile (null) } }, [material]) @@ -71,42 +70,40 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => { const updateMutation = useMutation ({ mutationFn: async () => { - const formData = new FormData - if (tag.trim ()) - formData.append ('tag', tag) - if (file) - formData.append ('file', file) - if (url.trim ()) - formData.append ('url', url) - formData.append ('export_paths[legacy_drive]', exportPath) + const formData = new FormData + if (tag.trim ()) + formData.append ('tag', tag) + if (file) + formData.append ('file', file) + if (url.trim ()) + formData.append ('url', url) + formData.append ('export_paths[legacy_drive]', exportPath) - return await updateMaterial (id ?? '', formData) + return await updateMaterial (id ?? '', formData) }, onSuccess: async data => { - qc.setQueryData (materialsKeys.show (id ?? ''), data) - await invalidateMaterialQueries () - toast ({ title: '更新成功!' }) + qc.setQueryData (materialsKeys.show (id ?? ''), data) + await invalidateMaterialQueries () + toast ({ title: '更新成功!' }) }, onError: error => { - applyValidationError (error) - toast ({ title: '更新失敗……', description: '入力を見直してください.' }) - }, - }) + applyValidationError (error) + toast ({ title: '更新失敗……', description: '入力を見直してください.' }) + }}) const suppressMutation = useMutation ({ mutationFn: async (reason: string) => - await suppressMaterialFile (id ?? '', { reason }), + await suppressMaterialFile (id ?? '', { reason }), onSuccess: async data => { - qc.setQueryData (materialsKeys.show (id ?? ''), data) - setFile (null) - setFilePreview ('') - await invalidateMaterialQueries () - toast ({ title: '抑止しました' }) + qc.setQueryData (materialsKeys.show (id ?? ''), data) + setFile (null) + setFilePreview ('') + await invalidateMaterialQueries () + toast ({ title: '抑止しました' }) }, onError: () => { - toast ({ title: '抑止に失敗しました' }) - }, - }) + toast ({ title: '抑止に失敗しました' }) + }}) const handleSubmit = () => { clearValidationErrors () @@ -125,155 +122,153 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => { return ( - {material && ( - - {`${ materialTitle } 素材照会 | ${ SITE_TITLE }`} - )} + {material && ( + + {`${ materialTitle } 素材照会 | ${ SITE_TITLE }`} + )} - {isLoading ? 'Loading...' : isError ? ( -

- 素材の取得に失敗しました. -

- ) : material == null ? ( -

- 素材が見つかりませんでした. -

- ) : ( - <> - - {material.tag - ? ( - ) - : materialTitle} - + {isLoading ? 'Loading...' : isError ? ( +

+ 素材の取得に失敗しました. +

) : material == null ? ( +

+ 素材が見つかりませんでした. +

) : ( + <> + + {material.tag + ? ( + ) + : materialTitle} + - {material.fileSuppressedAt && ( -
- 素材ファイルは抑止済みです。 - {material.fileSuppressionReason && ( - 理由: {material.fileSuppressionReason})} -
)} + {material.fileSuppressedAt && ( +
+ 素材ファイルは抑止済みです。 + {material.fileSuppressionReason && ( + 理由: {material.fileSuppressionReason})} +
)} - {(!material.fileSuppressedAt && material.file && material.contentType) && ( - (/image\/.*/.test (material.contentType) && ( - {material.tag?.name)) - || (/video\/.*/.test (material.contentType) && ( -
) } diff --git a/frontend/src/pages/materials/MaterialListPage.tsx b/frontend/src/pages/materials/MaterialListPage.tsx index a7c60f5..0c00a3e 100644 --- a/frontend/src/pages/materials/MaterialListPage.tsx +++ b/frontend/src/pages/materials/MaterialListPage.tsx @@ -25,8 +25,7 @@ import type { MaterialIndexSort, MaterialIndexSuppression, MaterialIndexTagState, - MaterialIndexView, -} from '@/types' + MaterialIndexView } from '@/types' const MEDIA_KIND_LABELS: Record = { image: '画像', @@ -34,8 +33,7 @@ const MEDIA_KIND_LABELS: Record = { audio: '音声', file_other: 'その他ファイル', url_only: 'URL のみ', - suppressed: '抑止済み', -} + suppressed: '抑止済み'} const MEDIA_FILTER_LABELS: Record = { all: 'すべて', @@ -43,8 +41,7 @@ const MEDIA_FILTER_LABELS: Record = { video: '動画', audio: '音声', file_other: 'その他ファイル', - url_only: 'URL のみ', -} + url_only: 'URL のみ'} const SORT_LABELS: Record = { created_at: '作成日時', @@ -53,8 +50,7 @@ const SORT_LABELS: Record = { media_kind: '種類', file_byte_size: 'ファイルサイズ', version_no: 'バージョン', - id: 'ID', -} + id: 'ID'} const setIf = (qs: URLSearchParams, key: string, value: string | null) => { @@ -67,8 +63,7 @@ const setIf = (qs: URLSearchParams, key: string, value: string | null) => { const parseOption = ( value: string | null, allowed: readonly T[], - fallback: T, -): T => allowed.includes (value as T) ? value as T : fallback + fallback: T): T => allowed.includes (value as T) ? value as T : fallback const fileSizeText = (bytes: number | null): string => { @@ -88,92 +83,88 @@ const materialTitle = (material: Material): string => const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
+ className={`flex aspect-square h-[180px] w-[180px] items-center justify-center + overflow-hidden rounded-lg border text-center shadow-sm ${ + material.fileSuppressedAt + ? [ + 'border-red-300 bg-red-50 text-red-900 dark:border-red-800', + 'dark:bg-red-950 dark:text-red-100'].join (' ') + : [ + 'border-stone-200 bg-white text-stone-900 dark:border-stone-700', + 'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}> {material.thumbnail ? : ( - - {material.thumbnailFallbackText} - )} + {material.thumbnailFallbackText} + )}
) const MaterialCard: FC<{ material: Material }> = ({ material }) => (
- -
-

- {materialTitle (material)} -

-

- {MEDIA_KIND_LABELS[material.mediaKind]} / {dateString (material.createdAt)} -

-
+ +
+

+ {materialTitle (material)} +

+

+ {MEDIA_KIND_LABELS[material.mediaKind]} / {dateString (material.createdAt)} +

+
) const MaterialListItem: FC<{ material: Material }> = ({ material }) => (
+ className={`rounded-lg border p-3 shadow-sm ${ + material.fileSuppressedAt + ? [ + 'border-red-200 bg-red-50 text-red-900 dark:border-red-900', + 'dark:bg-red-950 dark:text-red-100'].join (' ') + : [ + 'border-stone-200 bg-white text-stone-900 dark:border-stone-700', + 'dark:bg-stone-900 dark:text-stone-100'].join (' ')}`}>
- -
-
- - {materialTitle (material)} - - {material.fileSuppressedAt && ( -

抑止済み

)} -
-
-
-
種類:
-
{MEDIA_KIND_LABELS[material.mediaKind]}
-
- {material.fileByteSize != null && ( -
-
サイズ:
-
{fileSizeText (material.fileByteSize)}
-
)} - {material.url && ( -
-
URL:
-
{material.url}
-
)} -
-
作成:
-
{dateString (material.createdAt)}
-
-
-
+ +
+
+ + {materialTitle (material)} + + {material.fileSuppressedAt && ( +

抑止済み

)} +
+
+
+
種類:
+
{MEDIA_KIND_LABELS[material.mediaKind]}
+
+ {material.fileByteSize != null && ( +
+
サイズ:
+
{fileSizeText (material.fileByteSize)}
+
)} + {material.url && ( +
+
URL:
+
{material.url}
+
)} +
+
作成:
+
{dateString (material.createdAt)}
+
+
+
) @@ -190,9 +181,9 @@ const MaterialListPage: FC = () => { const tagState = query.get ('unclassified') === '1' ? 'untagged' : parseOption ( - query.get ('tag_state'), - ['all', 'tagged', 'untagged'], - 'all') + query.get ('tag_state'), + ['all', 'tagged', 'untagged'], + 'all') const mediaKind = parseOption ( query.get ('media_kind'), ['all', 'image', 'video', 'audio', 'file_other', 'url_only'], @@ -239,12 +230,10 @@ const MaterialListPage: FC = () => { direction, view, page, - limit, - } + limit} const { data, isLoading, isError } = useQuery ({ queryKey: materialsKeys.index (keys), - queryFn: () => fetchMaterials (keys), - }) + queryFn: () => fetchMaterials (keys)}) const materials = data?.materials ?? [] const totalPages = data ? Math.ceil (data.count / limit) : 0 @@ -289,214 +278,203 @@ const MaterialListPage: FC = () => { return ( - - - {`素材一覧 | ${ SITE_TITLE }`} - + + + {`素材一覧 | ${ SITE_TITLE }`} + -
-
- 素材一覧 -
- - 未分類素材 - - - 新規素材を追加 - - - ZIP をダウンロード - -
-
+
+
+ 素材一覧 +
+ + 新規素材を追加 + + + ZIP をダウンロード + +
+
- {tagState === 'untagged' && ( - - 素材検索トップへ戻る - )} + {tagState === 'untagged' && ( + + 素材検索トップへ戻る + )} -
-
- - {({ invalid }) => ( - setQ (e.target.value)} - placeholder="タグ名 / URL / ファイル名" - className={inputClass (invalid)}/>)} - + +
+ + {({ invalid }) => ( + setQ (e.target.value)} + placeholder="タグ名 / URL / ファイル名" + className={inputClass (invalid)}/>)} + - - {({ invalid }) => ( - )} - + + {({ invalid }) => ( + )} + - - {({ invalid }) => ( - )} - + + {({ invalid }) => ( + )} + - - {({ invalid }) => ( - )} - + + {({ invalid }) => ( + )} + - - {() => ( -
- - - -
)} -
+ + {() => ( +
+ + + +
)} +
- - {() => ( -
- - - -
)} -
-
+ + {() => ( +
+ + + +
)} +
+
-
- -
-
+
+ +
+ -
-
- - -
+
+
+ + +
-
- - -
-
+
+ + +
+
- {isLoading &&

Loading...

} - {isError && ( -

素材一覧の取得に失敗しました.

)} - {(!isLoading && !isError && materials.length === 0) && ( -

素材はありません.

)} - {materials.length > 0 && ( - view === 'card' - ? ( -
- {materials.map (material => ( - ))} -
) - : ( -
- {materials.map (material => ( - ))} -
))} - -
+ {isLoading &&

Loading...

} + {isError && ( +

素材一覧の取得に失敗しました.

)} + {(!isLoading && !isError && materials.length === 0) && ( +

素材はありません.

)} + {materials.length > 0 && ( + view === 'card' + ? ( +
+ {materials.map (material => ( + ))} +
) + : ( +
+ {materials.map (material => ( + ))} +
))} + +
) } diff --git a/frontend/src/pages/materials/MaterialNewPage.tsx b/frontend/src/pages/materials/MaterialNewPage.tsx index c65326b..18421d3 100644 --- a/frontend/src/pages/materials/MaterialNewPage.tsx +++ b/frontend/src/pages/materials/MaterialNewPage.tsx @@ -40,27 +40,26 @@ const MaterialNewPage: FC = () => { const createMutation = useMutation ({ mutationFn: async () => { - const formData = new FormData - if (tag) - formData.append ('tag', tag) - if (file) - formData.append ('file', file) - if (url) - formData.append ('url', url) - formData.append ('export_paths[legacy_drive]', exportPath) + const formData = new FormData + if (tag) + formData.append ('tag', tag) + if (file) + formData.append ('file', file) + if (url) + formData.append ('url', url) + formData.append ('export_paths[legacy_drive]', exportPath) - return await createMaterial (formData) + return await createMaterial (formData) }, onSuccess: async () => { - await qc.invalidateQueries ({ queryKey: materialsKeys.root }) - toast ({ title: '送信成功!' }) - navigate (`/materials?tag=${ encodeURIComponent (tag) }`) + await qc.invalidateQueries ({ queryKey: materialsKeys.root }) + toast ({ title: '送信成功!' }) + navigate (`/materials?tag=${ encodeURIComponent (tag) }`) }, onError: error => { - applyValidationError (error) - toast ({ title: '送信失敗……', description: '入力を見直してください.' }) - }, - }) + applyValidationError (error) + toast ({ title: '送信失敗……', description: '入力を見直してください.' }) + }}) const handleSubmit = () => { clearValidationErrors () diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx index 7c7f958..e5e823d 100644 --- a/frontend/src/pages/posts/PostDetailPage.tsx +++ b/frontend/src/pages/posts/PostDetailPage.tsx @@ -62,8 +62,7 @@ const PostDetailPage: FC = ({ user }) => { toast ({ title: '失敗……', description: '通信に失敗しました……' }) }, onSuccess: () => { - qc.invalidateQueries ({ queryKey: postsKeys.root }) - } }) + qc.invalidateQueries ({ queryKey: postsKeys.root })} }) useEffect (() => { if (!(errorFlg)) @@ -114,8 +113,7 @@ const PostDetailPage: FC = ({ user }) => { ({ ...p, parentPosts: [{ } as Post] }))]}/> -
- )} + )} {(post.parentPosts ?? []).map (pp => { const siblings = post.siblingPosts?.[String (pp.id) as `${ number }`] if (!(siblings)) diff --git a/frontend/src/pages/tags/NicoTagListPage.tsx b/frontend/src/pages/tags/NicoTagListPage.tsx index 39ebd93..5cbaadc 100644 --- a/frontend/src/pages/tags/NicoTagListPage.tsx +++ b/frontend/src/pages/tags/NicoTagListPage.tsx @@ -1,6 +1,6 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' 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 { useLocation, useNavigate } from 'react-router-dom' @@ -87,8 +87,7 @@ const NicoTagListPage: FC = ({ user }) => { const defaultDirection = { name: 'asc', created_at: 'desc', - updated_at: 'desc', - } as const + updated_at: 'desc'} as const const beginEdit = async (tag: NicoTag) => { const editingTag = nicoTags.find (tag => tag.id === editingId) @@ -99,15 +98,13 @@ const NicoTagListPage: FC = ({ user }) => { && !(await dialogue.confirm ({ title: '編集中の内容を破棄しますか?', confirmText: '破棄', - variant: 'danger', - }))) + variant: 'danger'}))) return setEditingId (tag.id) setRawTags (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]: [] })) } @@ -115,8 +112,7 @@ const NicoTagListPage: FC = ({ user }) => { setEditingId (null) setRawTags (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]: [] })) } @@ -141,8 +137,7 @@ const NicoTagListPage: FC = ({ user }) => { ...errors, [id]: validationError?.fieldErrors.tags ?? validationError?.baseErrors - ?? ['更新できませんでした.'], - })) + ?? ['更新できませんでした.']})) toast ({ title: '更新失敗', description: '入力内容を確認してください.' }) } finally @@ -166,8 +161,7 @@ const NicoTagListPage: FC = ({ user }) => { setRawTags (Object.fromEntries (data.tags.map (tag => [ tag.id, - tag.linkedTags.map (linkedTag => linkedTag.name).join (' '), - ]))) + tag.linkedTags.map (linkedTag => linkedTag.name).join (' ')]))) }, [data]) useEffect (() => { @@ -282,7 +276,8 @@ const NicoTagListPage: FC = ({ user }) => { {nicoTags.map (tag => { const isEditing = editingId === tag.id - return [ + return ( + = ({ user }) => { 編集 )} )} - , - isEditing && ( + + {isEditing && ( @@ -340,8 +335,7 @@ const NicoTagListPage: FC = ({ user }) => { placeholder="タグ名を空白または改行で区切って入力" onChange={e => setRawTags (rawTags => ({ ...rawTags, - [tag.id]: e.target.value, - }))}/> + [tag.id]: e.target.value}))}/>
- ), - ] + )} +
) })} diff --git a/frontend/src/pages/theatres/TheatreDetailPage.tsx b/frontend/src/pages/theatres/TheatreDetailPage.tsx index 2a206b6..f3f27aa 100644 --- a/frontend/src/pages/theatres/TheatreDetailPage.tsx +++ b/frontend/src/pages/theatres/TheatreDetailPage.tsx @@ -69,8 +69,7 @@ const userName = (user: Pick | null | undefined): string => const commentBox = ( comment: TheatreComment, - programme: TheatreProgramme | null = null, -): ReactNode[] => + programme: TheatreProgramme | null = null): ReactNode[] => [(
{comment.deleted @@ -120,8 +119,7 @@ const tagsByCategory = (tags: Tag[]): Partial> => { const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = ( - { tags, compact, flow = 'vertical' }, -) => { + { tags, compact, flow = 'vertical' }) => { const grouped = tagsByCategory (tags) if (flow === 'horizontal') diff --git a/frontend/src/stores/sharedTransitionStore.ts b/frontend/src/stores/sharedTransitionStore.ts index c0a7e98..b743693 100644 --- a/frontend/src/stores/sharedTransitionStore.ts +++ b/frontend/src/stores/sharedTransitionStore.ts @@ -15,5 +15,4 @@ export const useSharedTransitionStore = create (set => ({ set (state => { const next = { ...state.byLocationKey } delete next[locationKey] - return { byLocationKey: next } - }) })) + return { byLocationKey: next }}) }))