diff --git a/src/components/pages/cards-translation-print/CardsTranslationPrintList/CardsTranslationPrintListItem/index.module.scss b/src/components/pages/cards-translation-print/CardsTranslationPrintList/CardsTranslationPrintListItem/index.module.scss new file mode 100644 index 0000000..0f5fc56 --- /dev/null +++ b/src/components/pages/cards-translation-print/CardsTranslationPrintList/CardsTranslationPrintListItem/index.module.scss @@ -0,0 +1,24 @@ +.CardsTableCell { + box-sizing: border-box; + width: 55mm; + height: 40mm; + border: .5mm solid gray; + border-collapse: collapse; + text-align: left; + vertical-align: top; + font-family: 'Noto Sans JP'; + font-size: 3.3mm; + overflow: hidden; +} + +.CardsTableCellLongText { + font-size: 2.8mm; +} + +.CardsTableCellLongLongText { + font-size: 2.5mm; +} + +.CardTitle { + font-weight: bold; +} diff --git a/src/components/pages/cards-translation-print/CardsTranslationPrintList/CardsTranslationPrintListItem/index.tsx b/src/components/pages/cards-translation-print/CardsTranslationPrintList/CardsTranslationPrintListItem/index.tsx new file mode 100644 index 0000000..e9c773d --- /dev/null +++ b/src/components/pages/cards-translation-print/CardsTranslationPrintList/CardsTranslationPrintListItem/index.tsx @@ -0,0 +1,37 @@ +import classNames from 'classnames' +import type { FC } from 'react' + +import type { CardForPrint } from '@/libs/domain/Card' + +import styles from './index.module.scss' + +type CardsTranslationPrintListItemProps = Readonly<{ + card: CardForPrint +}> + +const CardsTranslationPrintListItem: FC = ({ card }) => { + const textLength = (card.prerequisite?.length ?? 0) + (card.description?.length ?? 0) + return ( + = 100, + [styles.CardsTableCellLongLongText]: textLength >= 130, + })} + > + + [{card.printedID}] {card.nameJa} + +
+ {card.prerequisite && ( + <> + (前提) {card.prerequisite} +
+ + )} + {card.description} + + ) +} + +export default CardsTranslationPrintListItem diff --git a/src/components/pages/cards-translation-print/CardsTranslationPrintList/index.module.scss b/src/components/pages/cards-translation-print/CardsTranslationPrintList/index.module.scss new file mode 100644 index 0000000..823a94f --- /dev/null +++ b/src/components/pages/cards-translation-print/CardsTranslationPrintList/index.module.scss @@ -0,0 +1,10 @@ +.CardsTable { + width: 100%; + border: .5mm gray solid; + border-collapse: collapse; +} + +.CardsTableRow { + break-inside: avoid-page; + border-collapse: collapse; +} diff --git a/src/components/pages/cards-translation-print/CardsTranslationPrintList/index.tsx b/src/components/pages/cards-translation-print/CardsTranslationPrintList/index.tsx new file mode 100644 index 0000000..0f84dd5 --- /dev/null +++ b/src/components/pages/cards-translation-print/CardsTranslationPrintList/index.tsx @@ -0,0 +1,74 @@ +import { GraphQLClient } from 'graphql-request' +import { type FC, useEffect, useState } from 'react' + +import { getSdk } from '@/libs/api/generated' +import { paramsToSearchCondition, searchConditionToWhere } from '@/libs/cards/search' +import type { CardForPrint } from '@/libs/domain/Card' +import type { RevisionKey } from '@/libs/domain/Revision' +import { isNonNullable } from '@/libs/utils/types' + +import CardsTranslationPrintListItem from './CardsTranslationPrintListItem' +import styles from './index.module.scss' + +type CardsTranslationPrintListProps = Readonly<{ + revisionKey: RevisionKey +}> + +const CardsTranslationPrintList: FC = ({ revisionKey }) => { + const [cards, setCards] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isLoaded, setIsLoaded] = useState(false) + + const params = new URLSearchParams(window.location.search) + const searchCondition = paramsToSearchCondition(params) + const where = searchConditionToWhere(revisionKey, searchCondition) + + const client = new GraphQLClient('https://api.db.agricolajp.dev/graphql') + const sdk = getSdk(client) + + useEffect(() => { + const fetch = async () => { + if (isLoading) return + setIsLoading(true) + const res = await sdk.GetCardsListForPrint({ where }) + const cards = res.cards?.edges?.map(e => e?.node).filter(isNonNullable) ?? [] + setCards(cards) + setIsLoading(false) + setIsLoaded(true) + } + fetch() + }, []) + + useEffect(() => { + if (isLoaded) { + window.print() + } + }, [isLoaded]) + + const cardsChunks = cards.reduce( + (acc: CardForPrint[][], _c, i) => (i % 3 ? acc : [...acc, ...[cards.slice(i, i + 3)]]), + [], + ) + + return ( + <> + {isLoading ? ( +

読込中です。しばらくお待ちください

+ ) : ( + + + {cardsChunks.map(cardsChunk => ( + + {cardsChunk.map(card => ( + + ))} + + ))} + +
+ )} + + ) +} + +export default CardsTranslationPrintList diff --git a/src/components/pages/cards/CardsExplorer/CardsSearchForm/index.tsx b/src/components/pages/cards/CardsExplorer/CardsSearchForm/index.tsx index 89931c4..3040dfe 100644 --- a/src/components/pages/cards/CardsExplorer/CardsSearchForm/index.tsx +++ b/src/components/pages/cards/CardsExplorer/CardsSearchForm/index.tsx @@ -9,11 +9,10 @@ import { import { Button, FloatingLabel, Form } from 'react-bootstrap' import { FaSearch } from 'react-icons/fa' +import type { CardTypeCondition, CardsSearchCondition } from '@/libs/domain/CardsSearchCondition' import type { DeckSummary } from '@/libs/domain/Deck' import type { ProductSummary } from '@/libs/domain/Product' -import type { CardTypeCondition, CardsSearchCondition } from '..' - type CardsSearchFormProps = Readonly<{ decks: Readonly products: Readonly diff --git a/src/components/pages/cards/CardsExplorer/index.tsx b/src/components/pages/cards/CardsExplorer/index.tsx index 64ea2a6..78f1612 100644 --- a/src/components/pages/cards/CardsExplorer/index.tsx +++ b/src/components/pages/cards/CardsExplorer/index.tsx @@ -1,90 +1,23 @@ import classNames from 'classnames' import { GraphQLClient } from 'graphql-request' import { type FC, useCallback, useEffect, useRef, useState } from 'react' -import { Col, Row, Spinner } from 'react-bootstrap' +import { Button, Col, Row, Spinner } from 'react-bootstrap' +import { FaPrint, FaRegCircleCheck } from 'react-icons/fa6' import Headline2 from '@/components/common/Headline2' -import { - type CardTypeWhereInput, - type CardWhereInput, - type PageInfo, - getSdk, -} from '@/libs/api/generated' +import { type PageInfo, getSdk } from '@/libs/api/generated' +import { paramsToSearchCondition, searchConditionToWhere } from '@/libs/cards/search' import type { CardSummary } from '@/libs/domain/Card' +import type { CardsSearchCondition } from '@/libs/domain/CardsSearchCondition' import type { DeckSummary } from '@/libs/domain/Deck' import type { ProductSummary } from '@/libs/domain/Product' import type { RevisionKey } from '@/libs/domain/Revision' -import { isNonNullable, unreachable } from '@/libs/utils/types' +import { isNonNullable } from '@/libs/utils/types' import CardsList from './CardsList' import CardsSearchForm from './CardsSearchForm' import styles from './index.module.scss' -const cardTypeConditions = ['occupation', 'minor_improvement', 'major_improvement', 'misc'] as const -export type CardTypeCondition = (typeof cardTypeConditions)[number] - -export type CardsSearchCondition = Readonly<{ - productID: string | undefined - deckID: string | undefined - cardType: CardTypeCondition | undefined - nameJa: string | undefined - nameEn: string | undefined - description: string | undefined -}> - -const paramsToSearchCondition = (params: URLSearchParams): CardsSearchCondition => { - const productID = params.get('productID') - const deckID = params.get('deckID') - const cardType = params.get('cardType') - const nameJa = params.get('nameJa') - const nameEn = params.get('nameEn') - const description = params.get('description') - const isCardType = (cardType: string): cardType is CardTypeCondition => - cardTypeConditions.some(c => c === cardType) - return { - productID: productID !== null ? productID : undefined, - deckID: deckID !== null ? deckID : undefined, - cardType: cardType !== null && isCardType(cardType) ? cardType : undefined, - nameJa: nameJa !== null ? nameJa : undefined, - nameEn: nameEn !== null ? nameEn : undefined, - description: description !== null ? description : undefined, - } -} - -const searchConditionToWhere = (revisionKey: string, searchCondition: CardsSearchCondition) => { - let hasCardTypeWith: CardTypeWhereInput['hasCardsWith'] - switch (searchCondition.cardType) { - case 'occupation': - hasCardTypeWith = [{ nameJa: '職業' }] - break - case 'minor_improvement': - hasCardTypeWith = [{ nameJa: '小さい進歩' }] - break - case 'major_improvement': - hasCardTypeWith = [{ nameJa: '大きい進歩' }] - break - case 'misc': - hasCardTypeWith = [{ nameJaNotIn: ['職業', '小さい進歩', '大きい進歩'] }] - break - case undefined: - break - default: - unreachable(searchCondition.cardType) - } - const where: CardWhereInput = { - hasRevisionWith: [{ key: revisionKey }], - hasProductsWith: - searchCondition.productID !== undefined ? [{ id: searchCondition.productID }] : undefined, - hasDeckWith: - searchCondition.deckID !== undefined ? [{ id: searchCondition.deckID }] : undefined, - hasCardTypeWith, - nameJaContains: searchCondition.nameJa, - nameEnContains: searchCondition.nameEn, - descriptionContains: searchCondition.description, - } - return where -} - type CardsExplorerProps = Readonly<{ revisionKey: RevisionKey decks: Readonly @@ -101,7 +34,7 @@ const CardsExplorer: FC = ({ revisionKey, decks, products }) const hasMore = pageInfo === undefined || pageInfo.hasNextPage const params = new URLSearchParams(window.location.search) - const searchCondition: CardsSearchCondition = paramsToSearchCondition(params) + const searchCondition = paramsToSearchCondition(params) const where = searchConditionToWhere(revisionKey, searchCondition) const client = new GraphQLClient('https://api.db.agricolajp.dev/graphql') @@ -150,6 +83,11 @@ const CardsExplorer: FC = ({ revisionKey, decks, products }) } }, [hasMore, spinnerRef, fetchMore]) + let translationPrintPath = `/${revisionKey}/cards-translation-print/` + if (window.location.search) { + translationPrintPath += `${window.location.search}` + } + return ( @@ -161,7 +99,16 @@ const CardsExplorer: FC = ({ revisionKey, decks, products }) onSubmit={onSubmitSearch} searchCondition={searchCondition} /> - {totalCount !== undefined &&

{totalCount}件ヒットしました

} + {totalCount !== undefined && ( + <> +

+ {totalCount}件ヒットしました +

+ + + )} diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 1921b4a..0f0941b 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -68,9 +68,9 @@ const ogImageAlt = ogImage?.alt ?? 'Agricola DB: Agricola Database for Japanese' ], }} /> - - + + + +
+ + diff --git a/src/layouts/print.scss b/src/layouts/print.scss new file mode 100644 index 0000000..c326ae6 --- /dev/null +++ b/src/layouts/print.scss @@ -0,0 +1,20 @@ +body { + margin: 0; + font-family: 'Noto Sans JP', -apple-system, BlinkMacSystemFont, 'Segoe UI', + 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@page { + size: A4 portrait; + margin: 20mm 0mm; +} + +.main { + width: 210mm; + height: 297mm; + box-sizing: border-box; + padding: 0 20mm; +} diff --git a/src/libs/api/documents/GetCardsListForPrint.graphql b/src/libs/api/documents/GetCardsListForPrint.graphql new file mode 100644 index 0000000..f71ebea --- /dev/null +++ b/src/libs/api/documents/GetCardsListForPrint.graphql @@ -0,0 +1,24 @@ +query GetCardsListForPrint($where: CardWhereInput!) { + cards(where: $where) { + edges { + node { + literalID + printedID + cardType { + key + } + revision { + id + key + } + nameJa + nameEn + minPlayersNumber + prerequisite + cost + description + } + } + totalCount + } +} diff --git a/src/libs/api/generated.ts b/src/libs/api/generated.ts index 05ec262..33db331 100644 --- a/src/libs/api/generated.ts +++ b/src/libs/api/generated.ts @@ -1,5 +1,4 @@ -import type { GraphQLClient } from 'graphql-request'; -import type { GraphQLClientRequestHeaders } from 'graphql-request/build/cjs/types'; +import type { GraphQLClient, RequestOptions } from 'graphql-request'; import gql from 'graphql-tag'; export type Maybe = T | null; export type InputMaybe = Maybe; @@ -8,6 +7,7 @@ export type MakeOptional = Omit & { [SubKey in K]?: export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; export type MakeEmpty = { [_ in K]?: never }; export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +type GraphQLClientRequestHeaders = RequestOptions['requestHeaders']; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: { input: string; output: string; } @@ -926,6 +926,13 @@ export type GetCardsListQueryVariables = Exact<{ export type GetCardsListQuery = { __typename?: 'Query', cards: { __typename?: 'CardConnection', totalCount: number, edges?: Array<{ __typename?: 'CardEdge', node?: { __typename?: 'Card', literalID: string, printedID?: string | null, nameJa?: string | null, nameEn?: string | null, cardType: { __typename?: 'CardType', key: string }, cardSpecialColor?: { __typename?: 'CardSpecialColor', key: string } | null, revision: { __typename?: 'Revision', id: string, key: string } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: any | null } } }; +export type GetCardsListForPrintQueryVariables = Exact<{ + where: CardWhereInput; +}>; + + +export type GetCardsListForPrintQuery = { __typename?: 'Query', cards: { __typename?: 'CardConnection', totalCount: number, edges?: Array<{ __typename?: 'CardEdge', node?: { __typename?: 'Card', literalID: string, printedID?: string | null, nameJa?: string | null, nameEn?: string | null, minPlayersNumber?: number | null, prerequisite?: string | null, cost?: string | null, description?: string | null, cardType: { __typename?: 'CardType', key: string }, revision: { __typename?: 'Revision', id: string, key: string } } | null } | null> | null } }; + export type GetDecksAndProductsListQueryVariables = Exact<{ [key: string]: never; }>; @@ -1042,6 +1049,32 @@ export const GetCardsListDocument = gql` } } `; +export const GetCardsListForPrintDocument = gql` + query GetCardsListForPrint($where: CardWhereInput!) { + cards(where: $where) { + edges { + node { + literalID + printedID + cardType { + key + } + revision { + id + key + } + nameJa + nameEn + minPlayersNumber + prerequisite + cost + description + } + } + totalCount + } +} + `; export const GetDecksAndProductsListDocument = gql` query GetDecksAndProductsList { decks { @@ -1064,7 +1097,7 @@ export const GetDecksAndProductsListDocument = gql` export type SdkFunctionWrapper = (action: (requestHeaders?:Record) => Promise, operationName: string, operationType?: string, variables?: any) => Promise; -const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, variables) => action(); +const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, _variables) => action(); export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) { return { @@ -1074,6 +1107,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = GetCardsList(variables: GetCardsListQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetCardsListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetCardsList', 'query', variables); }, + GetCardsListForPrint(variables: GetCardsListForPrintQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetCardsListForPrintDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetCardsListForPrint', 'query', variables); + }, GetDecksAndProductsList(variables?: GetDecksAndProductsListQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetDecksAndProductsListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetDecksAndProductsList', 'query', variables); } diff --git a/src/libs/cards/search/index.ts b/src/libs/cards/search/index.ts new file mode 100644 index 0000000..a848fb5 --- /dev/null +++ b/src/libs/cards/search/index.ts @@ -0,0 +1,63 @@ +import type { CardTypeWhereInput, CardWhereInput } from '@/libs/api/generated' +import { + type CardTypeCondition, + type CardsSearchCondition, + cardTypeConditions, +} from '@/libs/domain/CardsSearchCondition' +import { unreachable } from '@/libs/utils/types' + +export const paramsToSearchCondition = (params: URLSearchParams): CardsSearchCondition => { + const productID = params.get('productID') + const deckID = params.get('deckID') + const cardType = params.get('cardType') + const nameJa = params.get('nameJa') + const nameEn = params.get('nameEn') + const description = params.get('description') + const isCardType = (cardType: string): cardType is CardTypeCondition => + cardTypeConditions.some(c => c === cardType) + return { + productID: productID !== null ? productID : undefined, + deckID: deckID !== null ? deckID : undefined, + cardType: cardType !== null && isCardType(cardType) ? cardType : undefined, + nameJa: nameJa !== null ? nameJa : undefined, + nameEn: nameEn !== null ? nameEn : undefined, + description: description !== null ? description : undefined, + } +} + +export const searchConditionToWhere = ( + revisionKey: string, + searchCondition: CardsSearchCondition, +) => { + let hasCardTypeWith: CardTypeWhereInput['hasCardsWith'] + switch (searchCondition.cardType) { + case 'occupation': + hasCardTypeWith = [{ nameJa: '職業' }] + break + case 'minor_improvement': + hasCardTypeWith = [{ nameJa: '小さい進歩' }] + break + case 'major_improvement': + hasCardTypeWith = [{ nameJa: '大きい進歩' }] + break + case 'misc': + hasCardTypeWith = [{ nameJaNotIn: ['職業', '小さい進歩', '大きい進歩'] }] + break + case undefined: + break + default: + unreachable(searchCondition.cardType) + } + const where: CardWhereInput = { + hasRevisionWith: [{ key: revisionKey }], + hasProductsWith: + searchCondition.productID !== undefined ? [{ id: searchCondition.productID }] : undefined, + hasDeckWith: + searchCondition.deckID !== undefined ? [{ id: searchCondition.deckID }] : undefined, + hasCardTypeWith, + nameJaContains: searchCondition.nameJa, + nameEnContains: searchCondition.nameEn, + descriptionContains: searchCondition.description, + } + return where +} diff --git a/src/libs/changeLogs/index.ts b/src/libs/changeLogs/index.ts index a980e4a..b56a3a8 100644 --- a/src/libs/changeLogs/index.ts +++ b/src/libs/changeLogs/index.ts @@ -33,4 +33,9 @@ export const changeLogs: readonly ChangeLog[] = [ timestamp: parseISO('2024-01-09'), description: 'AgricolaDB および AgricolaDB API をリニューアルしました', }, + { + id: 5, + timestamp: parseISO('2024-05-19'), + description: 'カード一覧画面において翻訳シート印刷機能をリリースしました', + }, ] as const diff --git a/src/libs/domain/Card/index.ts b/src/libs/domain/Card/index.ts index 9d776cb..3539568 100644 --- a/src/libs/domain/Card/index.ts +++ b/src/libs/domain/Card/index.ts @@ -1,6 +1,10 @@ import type { UnwrapArray } from '@/libs/utils/types' -import type { GetCardsDetailByRevisionQuery, GetCardsListQuery } from '../../api/generated' +import type { + GetCardsDetailByRevisionQuery, + GetCardsListForPrintQuery, + GetCardsListQuery, +} from '../../api/generated' export type CardDetail = Readonly< NonNullable< @@ -11,3 +15,9 @@ export type CardDetail = Readonly< export type CardSummary = Readonly< NonNullable>>['node']> > + +export type CardForPrint = Readonly< + NonNullable< + NonNullable>>['node'] + > +> diff --git a/src/libs/domain/CardsSearchCondition/index.ts b/src/libs/domain/CardsSearchCondition/index.ts new file mode 100644 index 0000000..ab5e02e --- /dev/null +++ b/src/libs/domain/CardsSearchCondition/index.ts @@ -0,0 +1,17 @@ +export const cardTypeConditions = [ + 'occupation', + 'minor_improvement', + 'major_improvement', + 'misc', +] as const + +export type CardTypeCondition = (typeof cardTypeConditions)[number] + +export type CardsSearchCondition = Readonly<{ + productID: string | undefined + deckID: string | undefined + cardType: CardTypeCondition | undefined + nameJa: string | undefined + nameEn: string | undefined + description: string | undefined +}> diff --git a/src/pages/[revisionKey]/card/[literalID]/index.astro b/src/pages/[revisionKey]/card/[literalID]/index.astro index 3efbda7..d068398 100644 --- a/src/pages/[revisionKey]/card/[literalID]/index.astro +++ b/src/pages/[revisionKey]/card/[literalID]/index.astro @@ -61,7 +61,7 @@ const ogImage = { href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet + > diff --git a/src/pages/[revisionKey]/cards-translation-print/index.astro b/src/pages/[revisionKey]/cards-translation-print/index.astro new file mode 100644 index 0000000..5540090 --- /dev/null +++ b/src/pages/[revisionKey]/cards-translation-print/index.astro @@ -0,0 +1,18 @@ +--- +import CardsTranslationPrintList from '@/components/pages/cards-translation-print/CardsTranslationPrintList' +import PrintLayout from '@/layouts/PrintLayout.astro' +import { revisionKeys } from '@/libs/domain/Revision' + +export async function getStaticPaths() { + return revisionKeys.map(revisionKey => ({ + params: { revisionKey }, + })) +} + +const { revisionKey } = Astro.params +const { pathname } = Astro.url +--- + + + +