diff --git a/.github/workflows/create_heroku_review_app.yaml b/.github/workflows/create_heroku_review_app.yaml index 61605fb65..7e3ade03a 100644 --- a/.github/workflows/create_heroku_review_app.yaml +++ b/.github/workflows/create_heroku_review_app.yaml @@ -7,7 +7,7 @@ jobs: create-review-app: runs-on: ubuntu-latest steps: - - uses: fastruby/manage-heroku-review-app@9fa49f0320460f278c3687bc348dd0cbb18555dc # v1.3 + - uses: kqito/manage-heroku-review-app@55e434ad5ac86f21cf2f7654de1566973fbc7046 with: action: create env: diff --git a/.github/workflows/destroy_heroku_review_app.yaml b/.github/workflows/destroy_heroku_review_app.yaml index b2bf67949..cbcec744a 100644 --- a/.github/workflows/destroy_heroku_review_app.yaml +++ b/.github/workflows/destroy_heroku_review_app.yaml @@ -7,7 +7,7 @@ jobs: destroy-review-app: runs-on: ubuntu-latest steps: - - uses: fastruby/manage-heroku-review-app@9fa49f0320460f278c3687bc348dd0cbb18555dc # v1.3 + - uses: kqito/manage-heroku-review-app@55e434ad5ac86f21cf2f7654de1566973fbc7046 with: action: destroy env: diff --git a/public/images/001.jpeg b/public/images/001.jpeg deleted file mode 100644 index dc813003b..000000000 Binary files a/public/images/001.jpeg and /dev/null differ diff --git a/public/images/001.webp b/public/images/001.webp new file mode 100644 index 000000000..fbc0cf084 Binary files /dev/null and b/public/images/001.webp differ diff --git a/public/images/002.jpeg b/public/images/002.jpeg deleted file mode 100644 index 1ece17f3e..000000000 Binary files a/public/images/002.jpeg and /dev/null differ diff --git a/public/images/002.webp b/public/images/002.webp new file mode 100644 index 000000000..073153854 Binary files /dev/null and b/public/images/002.webp differ diff --git a/public/images/003.jpeg b/public/images/003.jpeg deleted file mode 100644 index ce28dbef0..000000000 Binary files a/public/images/003.jpeg and /dev/null differ diff --git a/public/images/003.webp b/public/images/003.webp new file mode 100644 index 000000000..e4780cf53 Binary files /dev/null and b/public/images/003.webp differ diff --git a/public/images/004.jpeg b/public/images/004.jpeg deleted file mode 100644 index 1ac258743..000000000 Binary files a/public/images/004.jpeg and /dev/null differ diff --git a/public/images/004.webp b/public/images/004.webp new file mode 100644 index 000000000..bd79b1d4f Binary files /dev/null and b/public/images/004.webp differ diff --git a/public/images/005.jpeg b/public/images/005.jpeg deleted file mode 100644 index 033c8cb54..000000000 Binary files a/public/images/005.jpeg and /dev/null differ diff --git a/public/images/005.webp b/public/images/005.webp new file mode 100644 index 000000000..f04526e01 Binary files /dev/null and b/public/images/005.webp differ diff --git a/public/images/006.jpeg b/public/images/006.jpeg deleted file mode 100644 index 254f8611b..000000000 Binary files a/public/images/006.jpeg and /dev/null differ diff --git a/public/images/006.webp b/public/images/006.webp new file mode 100644 index 000000000..461daea71 Binary files /dev/null and b/public/images/006.webp differ diff --git a/public/images/007.jpeg b/public/images/007.jpeg deleted file mode 100644 index f47135155..000000000 Binary files a/public/images/007.jpeg and /dev/null differ diff --git a/public/images/007.webp b/public/images/007.webp new file mode 100644 index 000000000..6eda8009f Binary files /dev/null and b/public/images/007.webp differ diff --git a/public/images/008.jpeg b/public/images/008.jpeg deleted file mode 100644 index 1b09e2d4e..000000000 Binary files a/public/images/008.jpeg and /dev/null differ diff --git a/public/images/008.webp b/public/images/008.webp new file mode 100644 index 000000000..c9b0d4376 Binary files /dev/null and b/public/images/008.webp differ diff --git a/public/images/009.jpeg b/public/images/009.jpeg deleted file mode 100644 index dddf48709..000000000 Binary files a/public/images/009.jpeg and /dev/null differ diff --git a/public/images/009.webp b/public/images/009.webp new file mode 100644 index 000000000..1949b56fa Binary files /dev/null and b/public/images/009.webp differ diff --git a/public/images/010.jpeg b/public/images/010.jpeg deleted file mode 100644 index cbb93cccd..000000000 Binary files a/public/images/010.jpeg and /dev/null differ diff --git a/public/images/010.webp b/public/images/010.webp new file mode 100644 index 000000000..5ee853d1d Binary files /dev/null and b/public/images/010.webp differ diff --git a/public/images/011.jpeg b/public/images/011.jpeg deleted file mode 100644 index a1daeebba..000000000 Binary files a/public/images/011.jpeg and /dev/null differ diff --git a/public/images/011.webp b/public/images/011.webp new file mode 100644 index 000000000..8d695e85e Binary files /dev/null and b/public/images/011.webp differ diff --git a/public/images/012.jpeg b/public/images/012.jpeg deleted file mode 100644 index c5426d6f9..000000000 Binary files a/public/images/012.jpeg and /dev/null differ diff --git a/public/images/012.webp b/public/images/012.webp new file mode 100644 index 000000000..7d8518a4d Binary files /dev/null and b/public/images/012.webp differ diff --git a/public/images/013.jpeg b/public/images/013.jpeg deleted file mode 100644 index 8ad516a13..000000000 Binary files a/public/images/013.jpeg and /dev/null differ diff --git a/public/images/013.webp b/public/images/013.webp new file mode 100644 index 000000000..fcdb2a93f Binary files /dev/null and b/public/images/013.webp differ diff --git a/public/images/014.jpeg b/public/images/014.jpeg deleted file mode 100644 index be184c423..000000000 Binary files a/public/images/014.jpeg and /dev/null differ diff --git a/public/images/014.webp b/public/images/014.webp new file mode 100644 index 000000000..a871bd203 Binary files /dev/null and b/public/images/014.webp differ diff --git a/public/images/015.jpeg b/public/images/015.jpeg deleted file mode 100644 index 7485bee90..000000000 Binary files a/public/images/015.jpeg and /dev/null differ diff --git a/public/images/015.webp b/public/images/015.webp new file mode 100644 index 000000000..1aeb36bd1 Binary files /dev/null and b/public/images/015.webp differ diff --git a/public/images/016.jpeg b/public/images/016.jpeg deleted file mode 100644 index 4cfbb87bd..000000000 Binary files a/public/images/016.jpeg and /dev/null differ diff --git a/public/images/016.webp b/public/images/016.webp new file mode 100644 index 000000000..73cbb3030 Binary files /dev/null and b/public/images/016.webp differ diff --git a/public/images/017.jpeg b/public/images/017.jpeg deleted file mode 100644 index 39a2f2408..000000000 Binary files a/public/images/017.jpeg and /dev/null differ diff --git a/public/images/017.webp b/public/images/017.webp new file mode 100644 index 000000000..13ce8c9e1 Binary files /dev/null and b/public/images/017.webp differ diff --git a/public/images/018.jpeg b/public/images/018.jpeg deleted file mode 100644 index f6ae9d20f..000000000 Binary files a/public/images/018.jpeg and /dev/null differ diff --git a/public/images/018.webp b/public/images/018.webp new file mode 100644 index 000000000..14b1f2c8d Binary files /dev/null and b/public/images/018.webp differ diff --git a/public/images/019.jpeg b/public/images/019.jpeg deleted file mode 100644 index 2f36db641..000000000 Binary files a/public/images/019.jpeg and /dev/null differ diff --git a/public/images/019.webp b/public/images/019.webp new file mode 100644 index 000000000..398c3234d Binary files /dev/null and b/public/images/019.webp differ diff --git a/public/images/020.jpeg b/public/images/020.jpeg deleted file mode 100644 index b4a566b73..000000000 Binary files a/public/images/020.jpeg and /dev/null differ diff --git a/public/images/020.webp b/public/images/020.webp new file mode 100644 index 000000000..8bd64849d Binary files /dev/null and b/public/images/020.webp differ diff --git a/public/images/021.jpeg b/public/images/021.jpeg deleted file mode 100644 index 6d663e7f9..000000000 Binary files a/public/images/021.jpeg and /dev/null differ diff --git a/public/images/021.webp b/public/images/021.webp new file mode 100644 index 000000000..0faf5cf70 Binary files /dev/null and b/public/images/021.webp differ diff --git a/public/images/022.jpeg b/public/images/022.jpeg deleted file mode 100644 index aa5cccf8c..000000000 Binary files a/public/images/022.jpeg and /dev/null differ diff --git a/public/images/022.webp b/public/images/022.webp new file mode 100644 index 000000000..849e9c965 Binary files /dev/null and b/public/images/022.webp differ diff --git a/public/images/023.jpeg b/public/images/023.jpeg deleted file mode 100644 index 1e1e4e7d3..000000000 Binary files a/public/images/023.jpeg and /dev/null differ diff --git a/public/images/023.webp b/public/images/023.webp new file mode 100644 index 000000000..750ee0148 Binary files /dev/null and b/public/images/023.webp differ diff --git a/public/images/024.jpeg b/public/images/024.jpeg deleted file mode 100644 index c5e5f86c8..000000000 Binary files a/public/images/024.jpeg and /dev/null differ diff --git a/public/images/024.webp b/public/images/024.webp new file mode 100644 index 000000000..ba27251f6 Binary files /dev/null and b/public/images/024.webp differ diff --git a/public/images/025.jpeg b/public/images/025.jpeg deleted file mode 100644 index e1c9bc113..000000000 Binary files a/public/images/025.jpeg and /dev/null differ diff --git a/public/images/025.webp b/public/images/025.webp new file mode 100644 index 000000000..f5b01ac14 Binary files /dev/null and b/public/images/025.webp differ diff --git a/public/images/026.jpeg b/public/images/026.jpeg deleted file mode 100644 index 9169af8dd..000000000 Binary files a/public/images/026.jpeg and /dev/null differ diff --git a/public/images/026.webp b/public/images/026.webp new file mode 100644 index 000000000..19591f3c1 Binary files /dev/null and b/public/images/026.webp differ diff --git a/public/images/027.jpeg b/public/images/027.jpeg deleted file mode 100644 index 754c29f51..000000000 Binary files a/public/images/027.jpeg and /dev/null differ diff --git a/public/images/027.webp b/public/images/027.webp new file mode 100644 index 000000000..3b3ce2abc Binary files /dev/null and b/public/images/027.webp differ diff --git a/public/images/028.jpeg b/public/images/028.jpeg deleted file mode 100644 index d27b403be..000000000 Binary files a/public/images/028.jpeg and /dev/null differ diff --git a/public/images/028.webp b/public/images/028.webp new file mode 100644 index 000000000..bf92d9f64 Binary files /dev/null and b/public/images/028.webp differ diff --git a/public/images/029.jpeg b/public/images/029.jpeg deleted file mode 100644 index e74fa0ed4..000000000 Binary files a/public/images/029.jpeg and /dev/null differ diff --git a/public/images/029.webp b/public/images/029.webp new file mode 100644 index 000000000..d884fef9a Binary files /dev/null and b/public/images/029.webp differ diff --git a/public/images/030.jpeg b/public/images/030.jpeg deleted file mode 100644 index 9d7fe74ff..000000000 Binary files a/public/images/030.jpeg and /dev/null differ diff --git a/public/images/030.webp b/public/images/030.webp new file mode 100644 index 000000000..4c1e92e6f Binary files /dev/null and b/public/images/030.webp differ diff --git a/public/images/031.jpeg b/public/images/031.jpeg deleted file mode 100644 index 926a3ee74..000000000 Binary files a/public/images/031.jpeg and /dev/null differ diff --git a/public/images/031.webp b/public/images/031.webp new file mode 100644 index 000000000..19b426566 Binary files /dev/null and b/public/images/031.webp differ diff --git a/public/images/032.jpeg b/public/images/032.jpeg deleted file mode 100644 index 57d8a4424..000000000 Binary files a/public/images/032.jpeg and /dev/null differ diff --git a/public/images/032.webp b/public/images/032.webp new file mode 100644 index 000000000..c7ae11451 Binary files /dev/null and b/public/images/032.webp differ diff --git a/public/images/033.jpeg b/public/images/033.jpeg deleted file mode 100644 index cb8d05eb9..000000000 Binary files a/public/images/033.jpeg and /dev/null differ diff --git a/public/images/033.webp b/public/images/033.webp new file mode 100644 index 000000000..7283ec529 Binary files /dev/null and b/public/images/033.webp differ diff --git a/public/images/034.jpeg b/public/images/034.jpeg deleted file mode 100644 index d96e76079..000000000 Binary files a/public/images/034.jpeg and /dev/null differ diff --git a/public/images/034.webp b/public/images/034.webp new file mode 100644 index 000000000..63169b825 Binary files /dev/null and b/public/images/034.webp differ diff --git a/public/images/035.jpeg b/public/images/035.jpeg deleted file mode 100644 index 0d3a506f3..000000000 Binary files a/public/images/035.jpeg and /dev/null differ diff --git a/public/images/035.webp b/public/images/035.webp new file mode 100644 index 000000000..b8dadb1ce Binary files /dev/null and b/public/images/035.webp differ diff --git a/public/images/036.jpeg b/public/images/036.jpeg deleted file mode 100644 index 1f349e9ab..000000000 Binary files a/public/images/036.jpeg and /dev/null differ diff --git a/public/images/036.webp b/public/images/036.webp new file mode 100644 index 000000000..0de5aed70 Binary files /dev/null and b/public/images/036.webp differ diff --git a/public/images/037.jpeg b/public/images/037.jpeg deleted file mode 100644 index b18bed87b..000000000 Binary files a/public/images/037.jpeg and /dev/null differ diff --git a/public/images/037.webp b/public/images/037.webp new file mode 100644 index 000000000..fc931830f Binary files /dev/null and b/public/images/037.webp differ diff --git a/workspaces/client/src/app/createStore.ts b/workspaces/client/src/app/createStore.ts index 92d2abaaf..1c66bbf30 100644 --- a/workspaces/client/src/app/createStore.ts +++ b/workspaces/client/src/app/createStore.ts @@ -1,5 +1,5 @@ import { withLenses } from '@dhmk/zustand-lens'; -import _ from 'lodash'; +import merge from 'lodash/merge'; import { createStore as createZustandStore } from 'zustand/vanilla'; import { createAuthStoreSlice } from '@wsh-2025/client/src/features/auth/stores/createAuthStoreSlice'; @@ -39,7 +39,7 @@ export const createStore = ({ hydrationData }: Props) => { })), ); - store.setState((s) => _.merge(s, hydrationData)); + store.setState((s) => merge(s, hydrationData)); return store; }; diff --git a/workspaces/client/src/features/auth/components/SignInDialog.tsx b/workspaces/client/src/features/auth/components/SignInDialog.tsx index 350d89edc..0cb318670 100644 --- a/workspaces/client/src/features/auth/components/SignInDialog.tsx +++ b/workspaces/client/src/features/auth/components/SignInDialog.tsx @@ -14,6 +14,17 @@ interface SignInFormValues { password: string; } +export const ErrorOutlineIcon = () => { + return ( + + + + ); +}; + interface Props { isOpen: boolean; onClose: () => void; @@ -121,7 +132,7 @@ export const SignInDialog = ({ isOpen, onClose, onOpenSignUp }: Props) => { {submitError ? (
-
+ {submitError}
) : null} diff --git a/workspaces/client/src/features/auth/components/SignOutDialog.tsx b/workspaces/client/src/features/auth/components/SignOutDialog.tsx index d7c1fd336..a97f620e3 100644 --- a/workspaces/client/src/features/auth/components/SignOutDialog.tsx +++ b/workspaces/client/src/features/auth/components/SignOutDialog.tsx @@ -1,6 +1,7 @@ import { FORM_ERROR } from 'final-form'; import { Form } from 'react-final-form'; +import { ErrorOutlineIcon } from '@wsh-2025/client/src/features/auth/components/SignInDialog'; import { useAuthActions } from '@wsh-2025/client/src/features/auth/hooks/useAuthActions'; import { Dialog } from '@wsh-2025/client/src/features/dialog/components/Dialog'; @@ -36,14 +37,23 @@ export const SignOutDialog = ({ isOpen, onClose }: Props) => {
{({ handleSubmit, submitError }) => ( void handleSubmit(ev)}> -
-
+
+ + + プレミアムエピソードが視聴できなくなります。
{submitError ? (
-
+ {submitError}
) : null} diff --git a/workspaces/client/src/features/auth/components/SignUpDialog.tsx b/workspaces/client/src/features/auth/components/SignUpDialog.tsx index 1cb939f8d..558b1a5c2 100644 --- a/workspaces/client/src/features/auth/components/SignUpDialog.tsx +++ b/workspaces/client/src/features/auth/components/SignUpDialog.tsx @@ -4,6 +4,7 @@ import { useId } from 'react'; import { Field, Form } from 'react-final-form'; import { z } from 'zod'; +import { ErrorOutlineIcon } from '@wsh-2025/client/src/features/auth/components/SignInDialog'; import { useAuthActions } from '@wsh-2025/client/src/features/auth/hooks/useAuthActions'; import { isValidEmail } from '@wsh-2025/client/src/features/auth/logics/isValidEmail'; import { isValidPassword } from '@wsh-2025/client/src/features/auth/logics/isValidPassword'; @@ -121,7 +122,7 @@ export const SignUpDialog = ({ isOpen, onClose, onOpenSignIn }: Props) => { {submitError ? (
-
+ {submitError}
) : null} diff --git a/workspaces/client/src/features/layout/components/Layout.tsx b/workspaces/client/src/features/layout/components/Layout.tsx index 99eadef4b..0dce57c43 100644 --- a/workspaces/client/src/features/layout/components/Layout.tsx +++ b/workspaces/client/src/features/layout/components/Layout.tsx @@ -63,7 +63,7 @@ export const Layout = ({ children }: Props) => { : 'bg-gradient-to-b from-[#171717] to-[#171717]', )} > - + AREMA @@ -75,9 +75,30 @@ export const Layout = ({ children }: Props) => { type="button" onClick={isSignedIn ? authActions.openSignOutDialog : authActions.openSignInDialog} > -
+
+ {isSignedIn ? ( + + + + ) : ( + + + + )} +
+ {isSignedIn ? 'ログアウト' : 'ログイン'} @@ -85,17 +106,38 @@ export const Layout = ({ children }: Props) => { -
+
+ + + + + + +
+ ホーム -
+
+ + + +
+ 番組表 diff --git a/workspaces/client/src/features/layout/components/Loading.tsx b/workspaces/client/src/features/layout/components/Loading.tsx index 4a61fe30c..9fb1bd2ae 100644 --- a/workspaces/client/src/features/layout/components/Loading.tsx +++ b/workspaces/client/src/features/layout/components/Loading.tsx @@ -1,7 +1,44 @@ +import { FC } from 'react'; + +interface SpinnerProps { + className?: string; + color?: string; + size?: number; +} + +export const Spinner: FC = ({ className = '', color = 'currentColor', size = 48 }) => { + return ( + + + + + + + + + + + + ); +}; + export const Loading = () => { return (
-
+
+ +
); }; diff --git a/workspaces/client/src/features/player/components/Player.tsx b/workspaces/client/src/features/player/components/Player.tsx index f1c27e8b9..7981613df 100644 --- a/workspaces/client/src/features/player/components/Player.tsx +++ b/workspaces/client/src/features/player/components/Player.tsx @@ -2,6 +2,7 @@ import { Ref, useEffect, useRef } from 'react'; import invariant from 'tiny-invariant'; import { assignRef } from 'use-callback-ref'; +import { Spinner } from '@wsh-2025/client/src/features/layout/components/Loading'; import { PlayerType } from '@wsh-2025/client/src/features/player/constants/player_type'; import { PlayerWrapper } from '@wsh-2025/client/src/features/player/interfaces/player_wrapper'; @@ -49,7 +50,7 @@ export const Player = ({ className, loop, playerRef, playerType, playlistUrl }:
-
+
diff --git a/workspaces/client/src/features/recommended/components/EpisodeItem.tsx b/workspaces/client/src/features/recommended/components/EpisodeItem.tsx index 74b08745c..497a21315 100644 --- a/workspaces/client/src/features/recommended/components/EpisodeItem.tsx +++ b/workspaces/client/src/features/recommended/components/EpisodeItem.tsx @@ -25,7 +25,7 @@ export const EpisodeItem = ({ episode }: Props) => { <>
- + {episode.premium ? ( diff --git a/workspaces/client/src/features/recommended/components/SeriesItem.tsx b/workspaces/client/src/features/recommended/components/SeriesItem.tsx index 2477b7a97..2847e5e96 100644 --- a/workspaces/client/src/features/recommended/components/SeriesItem.tsx +++ b/workspaces/client/src/features/recommended/components/SeriesItem.tsx @@ -21,7 +21,7 @@ export const SeriesItem = ({ series }: Props) => { <>
- +
diff --git a/workspaces/client/src/features/recommended/hooks/useRecommended.ts b/workspaces/client/src/features/recommended/hooks/useRecommended.ts index 97ee95c07..241995d6c 100644 --- a/workspaces/client/src/features/recommended/hooks/useRecommended.ts +++ b/workspaces/client/src/features/recommended/hooks/useRecommended.ts @@ -1,6 +1,7 @@ import { useStore } from '@wsh-2025/client/src/app/StoreContext'; interface Params { + limit?: number; referenceId: string; } diff --git a/workspaces/client/src/features/recommended/services/recommendedService.ts b/workspaces/client/src/features/recommended/services/recommendedService.ts index 494ad911f..a52eb1390 100644 --- a/workspaces/client/src/features/recommended/services/recommendedService.ts +++ b/workspaces/client/src/features/recommended/services/recommendedService.ts @@ -17,14 +17,15 @@ const $fetch = createFetch({ interface RecommendedService { fetchRecommendedModulesByReferenceId: (params: { + limit?: number; referenceId: string; }) => Promise>; } export const recommendedService: RecommendedService = { - async fetchRecommendedModulesByReferenceId({ referenceId }) { + async fetchRecommendedModulesByReferenceId({ limit, referenceId }) { const data = await $fetch('/recommended/:referenceId', { - params: { referenceId }, + params: { limit, referenceId }, }); return data; }, diff --git a/workspaces/client/src/features/recommended/stores/createRecomendedStoreSlice.ts b/workspaces/client/src/features/recommended/stores/createRecomendedStoreSlice.ts index aa2c858fc..3ce331a29 100644 --- a/workspaces/client/src/features/recommended/stores/createRecomendedStoreSlice.ts +++ b/workspaces/client/src/features/recommended/stores/createRecomendedStoreSlice.ts @@ -19,6 +19,7 @@ interface RecommendedState { interface RecommendedActions { fetchRecommendedModulesByReferenceId: (params: { + limit?: number; referenceId: ReferenceId; }) => Promise>; } diff --git a/workspaces/client/src/pages/episode/components/EpisodePage.tsx b/workspaces/client/src/pages/episode/components/EpisodePage.tsx index 075eacc48..370333eae 100644 --- a/workspaces/client/src/pages/episode/components/EpisodePage.tsx +++ b/workspaces/client/src/pages/episode/components/EpisodePage.tsx @@ -9,6 +9,7 @@ import { useAuthActions } from '@wsh-2025/client/src/features/auth/hooks/useAuth import { useAuthUser } from '@wsh-2025/client/src/features/auth/hooks/useAuthUser'; import { useEpisodeById } from '@wsh-2025/client/src/features/episode/hooks/useEpisodeById'; import { AspectRatio } from '@wsh-2025/client/src/features/layout/components/AspectRatio'; +import { Spinner } from '@wsh-2025/client/src/features/layout/components/Loading'; import { Player } from '@wsh-2025/client/src/features/player/components/Player'; import { PlayerType } from '@wsh-2025/client/src/features/player/constants/player_type'; import { RecommendedSection } from '@wsh-2025/client/src/features/recommended/components/RecommendedSection'; @@ -22,7 +23,7 @@ export const prefetch = async (store: ReturnType, { episodeI const episode = await store.getState().features.episode.fetchEpisodeById({ episodeId }); const modules = await store .getState() - .features.recommended.fetchRecommendedModulesByReferenceId({ referenceId: episodeId }); + .features.recommended.fetchRecommendedModulesByReferenceId({ limit: 1, referenceId: episodeId }); return { episode, modules }; }; @@ -36,7 +37,7 @@ export const EpisodePage = () => { const episode = useEpisodeById({ episodeId }); invariant(episode); - const modules = useRecommended({ referenceId: episodeId }); + const modules = useRecommended({ limit: 1, referenceId: episodeId }); const playerRef = usePlayerRef(); @@ -77,7 +78,9 @@ export const EpisodePage = () => { src={episode.thumbnailUrl} />
-
+
+ +
} diff --git a/workspaces/client/src/pages/episode/hooks/useSeekThumbnail.ts b/workspaces/client/src/pages/episode/hooks/useSeekThumbnail.ts index 8d0015d89..735f51a3b 100644 --- a/workspaces/client/src/pages/episode/hooks/useSeekThumbnail.ts +++ b/workspaces/client/src/pages/episode/hooks/useSeekThumbnail.ts @@ -21,9 +21,9 @@ async function getSeekThumbnail({ episode }: Params) { coreURL: await import('@ffmpeg/core?arraybuffer').then(({ default: b }) => { return URL.createObjectURL(new Blob([b], { type: 'text/javascript' })); }), - wasmURL: await import('@ffmpeg/core/wasm?arraybuffer').then(({ default: b }) => { - return URL.createObjectURL(new Blob([b], { type: 'application/wasm' })); - }), + // wasmURL: await import('@ffmpeg/core/wasm?arraybuffer').then(({ default: b }) => { + // return URL.createObjectURL(new Blob([b], { type: 'application/wasm' })); + // }), }); // 動画のセグメントファイルを取得 diff --git a/workspaces/client/src/pages/program/components/PlayerController.tsx b/workspaces/client/src/pages/program/components/PlayerController.tsx index 6a1c7fef0..b21f47fa3 100644 --- a/workspaces/client/src/pages/program/components/PlayerController.tsx +++ b/workspaces/client/src/pages/program/components/PlayerController.tsx @@ -10,8 +10,15 @@ export const PlayerController = () => {
-
- +
+ + + ライブ配信
diff --git a/workspaces/client/src/pages/program/components/ProgramPage.tsx b/workspaces/client/src/pages/program/components/ProgramPage.tsx index d13dc9426..dc4ac66c5 100644 --- a/workspaces/client/src/pages/program/components/ProgramPage.tsx +++ b/workspaces/client/src/pages/program/components/ProgramPage.tsx @@ -107,6 +107,7 @@ export const ProgramPage = () => {

この番組は放送が終了しました

見逃し視聴する diff --git a/workspaces/client/src/pages/timetable/components/ProgramDetailDialog.tsx b/workspaces/client/src/pages/timetable/components/ProgramDetailDialog.tsx index 41e0ec14c..7ff7e9616 100644 --- a/workspaces/client/src/pages/timetable/components/ProgramDetailDialog.tsx +++ b/workspaces/client/src/pages/timetable/components/ProgramDetailDialog.tsx @@ -55,6 +55,7 @@ export const ProgramDetailDialog = ({ isOpen, program }: Props): ReactElement =>
diff --git a/workspaces/client/src/pages/timetable/stores/createTimetablePageStoreSlice.ts b/workspaces/client/src/pages/timetable/stores/createTimetablePageStoreSlice.ts index ed190b39b..de9663f6e 100644 --- a/workspaces/client/src/pages/timetable/stores/createTimetablePageStoreSlice.ts +++ b/workspaces/client/src/pages/timetable/stores/createTimetablePageStoreSlice.ts @@ -2,7 +2,7 @@ import { lens } from '@dhmk/zustand-lens'; import { StandardSchemaV1 } from '@standard-schema/spec'; import * as schema from '@wsh-2025/schema/src/api/schema'; import { produce } from 'immer'; -import _ from 'lodash'; +import debounce from 'lodash/debounce'; import { ArrayValues } from 'type-fest'; import { DEFAULT_WIDTH } from '@wsh-2025/client/src/features/timetable/constants/grid_size'; @@ -41,7 +41,7 @@ export const createTimetablePageStoreSlice = () => { }, columnWidthRecord: {}, currentUnixtimeMs: 0, - refreshCurrentUnixtimeMs: _.debounce(() => { + refreshCurrentUnixtimeMs: debounce(() => { set(() => ({ currentUnixtimeMs: Date.now(), })); diff --git a/workspaces/client/src/setups/unocss.ts b/workspaces/client/src/setups/unocss.ts index d8e91da31..e3e1ef646 100644 --- a/workspaces/client/src/setups/unocss.ts +++ b/workspaces/client/src/setups/unocss.ts @@ -1,5 +1,3 @@ -import { IconifyJSON } from '@iconify/types'; -import presetIcons from '@unocss/preset-icons/browser'; import presetWind3 from '@unocss/preset-wind3'; import initUnocssRuntime, { defineConfig } from '@unocss/runtime'; @@ -45,24 +43,7 @@ async function init() { `, }, ], - presets: [ - presetWind3(), - presetIcons({ - collections: { - bi: () => import('@iconify/json/json/bi.json').then((m): IconifyJSON => m.default as IconifyJSON), - bx: () => import('@iconify/json/json/bx.json').then((m): IconifyJSON => m.default as IconifyJSON), - 'fa-regular': () => - import('@iconify/json/json/fa-regular.json').then((m): IconifyJSON => m.default as IconifyJSON), - 'fa-solid': () => - import('@iconify/json/json/fa-solid.json').then((m): IconifyJSON => m.default as IconifyJSON), - fluent: () => import('@iconify/json/json/fluent.json').then((m): IconifyJSON => m.default as IconifyJSON), - 'line-md': () => - import('@iconify/json/json/line-md.json').then((m): IconifyJSON => m.default as IconifyJSON), - 'material-symbols': () => - import('@iconify/json/json/material-symbols.json').then((m): IconifyJSON => m.default as IconifyJSON), - }, - }), - ], + presets: [presetWind3()], }), }); } diff --git a/workspaces/client/webpack.config.mjs b/workspaces/client/webpack.config.mjs index 9164a996e..16421b177 100644 --- a/workspaces/client/webpack.config.mjs +++ b/workspaces/client/webpack.config.mjs @@ -1,19 +1,22 @@ import path from 'node:path'; +// import CompressionPlugin from 'compression-webpack-plugin'; +// import TerserPlugin from 'terser-webpack-plugin'; import webpack from 'webpack'; +// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; /** @type {import('webpack').Configuration} */ const config = { - devtool: 'inline-source-map', + // 本番モードで最適化 + devtool: false, // ソースマップを無効化で軽量化 entry: './src/main.tsx', - mode: 'none', + mode: 'production', + module: { rules: [ { exclude: [/node_modules\/video\.js/, /node_modules\/@videojs/], - resolve: { - fullySpecified: false, - }, + resolve: { fullySpecified: false }, test: /\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/, use: { loader: 'babel-loader', @@ -23,8 +26,9 @@ const config = { '@babel/preset-env', { corejs: '3.41', - forceAllTransforms: true, - targets: 'defaults', + // 最新版のみ対象 + forceAllTransforms: false, + targets: 'last 1 version', useBuiltIns: 'entry', }, ], @@ -45,27 +49,33 @@ const config = { { resourceQuery: /arraybuffer/, type: 'javascript/auto', - use: { - loader: 'arraybuffer-loader', - }, + use: { loader: 'arraybuffer-loader' }, }, ], }, + + optimization: { + minimize: true, + usedExports: true, // 未使用コードの削除 + }, + output: { + // ハッシュ付きでキャッシュ防止 chunkFilename: 'chunk-[contenthash].js', chunkFormat: false, filename: 'main.js', path: path.resolve(import.meta.dirname, './dist'), publicPath: 'auto', }, + plugins: [ - new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), - new webpack.EnvironmentPlugin({ API_BASE_URL: '/api', NODE_ENV: '' }), + new webpack.EnvironmentPlugin({ API_BASE_URL: '/api', NODE_ENV: 'production' }), + // new BundleAnalyzerPlugin() ], resolve: { alias: { '@ffmpeg/core$': path.resolve(import.meta.dirname, 'node_modules', '@ffmpeg/core/dist/umd/ffmpeg-core.js'), - '@ffmpeg/core/wasm$': path.resolve(import.meta.dirname, 'node_modules', '@ffmpeg/core/dist/umd/ffmpeg-core.wasm'), + // '@ffmpeg/core/wasm$': path.resolve(import.meta.dirname, 'node_modules', '@ffmpeg/core/dist/umd/ffmpeg-core.wasm'), }, extensions: ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.tsx', '.jsx'], }, diff --git a/workspaces/schema/src/openapi/schema.ts b/workspaces/schema/src/openapi/schema.ts index fb64a2f3d..16157199b 100644 --- a/workspaces/schema/src/openapi/schema.ts +++ b/workspaces/schema/src/openapi/schema.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import * as databaseSchema from '@wsh-2025/schema/src/database/schema'; -function assertSchema(_actual: z.ZodType>, _expected: z.ZodType): void {} +function assertSchema(_actual: z.ZodType>, _expected: z.ZodType): void { } const channel = z.object({ id: z.string().openapi({ format: 'uuid' }), @@ -20,7 +20,7 @@ const episode = z.object({ title: z.string().openapi({ example: '第1話 吾輩は猫である' }), description: z.string().openapi({ example: - '『吾輩は猫である』(わがはいはねこである)は、夏目漱石の長編小説であり、処女小説である。1905年(明治38年)1月、『ホトトギス』にて発表されたのだが、好評を博したため、翌1906年(明治39年)8月まで継続した。上、1906年10月刊、中、1906年11月刊、下、1907年5月刊。この文章は、クリエイティブ・コモンズ 表示-継承 4.0 国際 パブリック・ライセンスのもとで公表されたウィキペディアの項目「吾輩は猫である」(https://ja.wikipedia.org/wiki/吾輩は猫である)を素材として二次利用しています。', + '『吾輩は猫である』(わがはいはねこである)は、', }), order: z.number().openapi({ example: 1 }), seriesId: z.string().openapi({ format: 'uuid' }), @@ -37,7 +37,7 @@ const series = z.object({ title: z.string().openapi({ example: '吾輩は猫である' }), description: z.string().openapi({ example: - '『吾輩は猫である』(わがはいはねこである)は、夏目漱石の長編小説であり、処女小説である。1905年(明治38年)1月、『ホトトギス』にて発表されたのだが、好評を博したため、翌1906年(明治39年)8月まで継続した。上、1906年10月刊、中、1906年11月刊、下、1907年5月刊。この文章は、クリエイティブ・コモンズ 表示-継承 4.0 国際 パブリック・ライセンスのもとで公表されたウィキペディアの項目「吾輩は猫である」(https://ja.wikipedia.org/wiki/吾輩は猫である)を素材として二次利用しています。', + '『吾輩は猫である』', }), thumbnailUrl: z.string().openapi({ example: 'https://image.example.com/assets/d13d2e22-a7ff-44ba-94a3-5f025f2b63cd.png', @@ -50,7 +50,7 @@ const program = z.object({ title: z.string().openapi({ example: '吾輩は猫である' }), description: z.string().openapi({ example: - '『吾輩は猫である』(わがはいはねこである)は、夏目漱石の長編小説であり、処女小説である。1905年(明治38年)1月、『ホトトギス』にて発表されたのだが、好評を博したため、翌1906年(明治39年)8月まで継続した。上、1906年10月刊、中、1906年11月刊、下、1907年5月刊。この文章は、クリエイティブ・コモンズ 表示-継承 4.0 国際 パブリック・ライセンスのもとで公表されたウィキペディアの項目「吾輩は猫である」(https://ja.wikipedia.org/wiki/吾輩は猫である)を素材として二次利用しています。', + '『吾輩は猫である』', }), startAt: z.string().openapi({ format: 'date-time' }), endAt: z.string().openapi({ format: 'date-time' }), @@ -176,6 +176,7 @@ export const getProgramByIdResponse = program.extend({ // GET /recommended/:referenceId export const getRecommendedModulesRequestParams = z.object({ + limit: z.number().optional(), referenceId: z.string(), }); export const getRecommendedModulesResponse = z.array( diff --git a/workspaces/server/database.sqlite b/workspaces/server/database.sqlite index 5afaf4e28..6a857d8a0 100644 Binary files a/workspaces/server/database.sqlite and b/workspaces/server/database.sqlite differ diff --git a/workspaces/server/src/api.ts b/workspaces/server/src/api.ts index 635624b1f..18903e480 100644 --- a/workspaces/server/src/api.ts +++ b/workspaces/server/src/api.ts @@ -476,6 +476,7 @@ export async function registerApi(app: FastifyInstance): Promise { where(module, { eq }) { return eq(module.referenceId, req.params.referenceId); }, + limit: req.params.limit, with: { items: { orderBy(item, { asc }) { diff --git a/workspaces/server/src/index.ts b/workspaces/server/src/index.ts index e02ab7613..1e2a13a9b 100644 --- a/workspaces/server/src/index.ts +++ b/workspaces/server/src/index.ts @@ -14,7 +14,7 @@ async function main() { const app = fastify(); app.addHook('onSend', async (_req, reply) => { - reply.header('cache-control', 'no-store'); + reply.header('cache-control', 'public, max-age=604800'); // 604800秒 = 1週間 }); app.register(cors, { origin: true, diff --git a/workspaces/server/src/ssr.tsx b/workspaces/server/src/ssr.tsx index 7603aafdd..afb3af4c3 100644 --- a/workspaces/server/src/ssr.tsx +++ b/workspaces/server/src/ssr.tsx @@ -1,4 +1,4 @@ -import { readdirSync } from 'node:fs'; +// import { readdirSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,22 +8,22 @@ import { createRoutes } from '@wsh-2025/client/src/app/createRoutes'; import { createStore } from '@wsh-2025/client/src/app/createStore'; import type { FastifyInstance } from 'fastify'; import { createStandardRequest } from 'fastify-standard-request-reply'; -import htmlescape from 'htmlescape'; +// import htmlescape from 'htmlescape'; import { StrictMode } from 'react'; import { renderToString } from 'react-dom/server'; import { createStaticHandler, createStaticRouter, StaticRouterProvider } from 'react-router'; -function getFiles(parent: string): string[] { - const dirents = readdirSync(parent, { withFileTypes: true }); - return dirents - .filter((dirent) => dirent.isFile() && !dirent.name.startsWith('.')) - .map((dirent) => path.join(parent, dirent.name)); -} +// function getFiles(parent: string): string[] { +// const dirents = readdirSync(parent, { withFileTypes: true }); +// return dirents +// .filter((dirent) => dirent.isFile() && !dirent.name.startsWith('.')) +// .map((dirent) => path.join(parent, dirent.name)); +// } -function getFilePaths(relativePath: string, rootDir: string): string[] { - const files = getFiles(path.resolve(rootDir, relativePath)); - return files.map((file) => path.join('/', path.relative(rootDir, file))); -} +// function getFilePaths(relativePath: string, rootDir: string): string[] { +// const files = getFiles(path.resolve(rootDir, relativePath)); +// return files.map((file) => path.join('/', path.relative(rootDir, file))); +// } export function registerSsr(app: FastifyInstance): void { app.register(fastifyStatic, { @@ -59,13 +59,13 @@ export function registerSsr(app: FastifyInstance): void { , ); - const rootDir = path.resolve(__dirname, '../../../'); - const imagePaths = [ + // const rootDir = path.resolve(__dirname, '../../../'); + /* const imagePaths = [ getFilePaths('public/images', rootDir), getFilePaths('public/animations', rootDir), getFilePaths('public/logos', rootDir), ].flat(); - + */ reply.type('text/html').send(/* html */ ` @@ -73,16 +73,9 @@ export function registerSsr(app: FastifyInstance): void { - ${imagePaths.map((imagePath) => ``).join('\n')} - `); }); } diff --git a/workspaces/server/tools/seed.ts b/workspaces/server/tools/seed.ts index 611eba167..fb776372f 100644 --- a/workspaces/server/tools/seed.ts +++ b/workspaces/server/tools/seed.ts @@ -8,8 +8,8 @@ import { DateTime } from 'luxon'; import { fetchAnimeList } from '@wsh-2025/server/tools/fetch_anime_list'; import { fetchLoremIpsumWordList } from '@wsh-2025/server/tools/fetch_lorem_ipsum_word_list'; import * as bcrypt from 'bcrypt'; -import path from 'node:path'; import { readdirSync } from 'node:fs'; +import path from 'node:path'; function getFiles(parent: string): string[] { const dirents = readdirSync(parent, { withFileTypes: true }); @@ -129,7 +129,7 @@ async function main() { const seriesList: (typeof schema.series.$inferSelect)[] = []; { const data: (typeof schema.series.$inferInsert)[] = Array.from({ length: 30 }, () => ({ - description: faker.lorem.paragraph({ max: 200, min: 100 }).replace(/\s/g, '').replace(/\./g, '。'), + description: faker.lorem.paragraph({ max: 1, min: 1 }).replace(/\s/g, '').replace(/\./g, '。'), id: faker.string.uuid(), thumbnailUrl: `${faker.helpers.arrayElement(imagePaths)}?version=${faker.string.nanoid()}`, title: faker.helpers.arrayElement(seriesTitleList), @@ -145,7 +145,7 @@ async function main() { const data: (typeof schema.episode.$inferInsert)[] = Array.from( { length: faker.number.int({ max: 20, min: 10 }) }, (_, idx) => ({ - description: faker.lorem.paragraph({ max: 200, min: 100 }).replace(/\s/g, '').replace(/\./g, '。'), + description: faker.lorem.paragraph({ max: 1, min: 1 }).replace(/\s/g, '').replace(/\./g, '。'), id: faker.string.uuid(), order: idx + 1, seriesId: series.id, @@ -162,7 +162,17 @@ async function main() { // Create programs console.log('Creating programs...'); const programList: (typeof schema.program.$inferInsert)[] = []; - const episodeListGroupedByStreamId = Object.values(Object.groupBy(episodeList, (episode) => episode.streamId)); + function groupBy(array: T[], key: (item: T) => string): Record { + return array.reduce((result, item) => { + const groupKey = key(item); + if (!result[groupKey]) { + result[groupKey] = []; + } + result[groupKey].push(item); + return result; + }, {} as Record); + } + const episodeListGroupedByStreamId = Object.values(groupBy(episodeList, (episode) => episode.streamId)); for (const channel of channelList) { let remainingMinutes = 24 * 60; let startAt = DateTime.now().startOf('day').toMillis(); @@ -178,7 +188,7 @@ async function main() { const series = seriesList.find((s) => s.id === episode.seriesId); const program: typeof schema.program.$inferInsert = { channelId: channel.id, - description: faker.lorem.paragraph({ max: 200, min: 100 }).replace(/\s/g, '').replace(/\./g, '。'), + description: faker.lorem.paragraph({ max: 1, min: 1 }).replace(/\s/g, '').replace(/\./g, '。'), endAt: new Date(endAt).toISOString(), episodeId: episode.id, id: faker.string.uuid(),