Skip to content
Open

wip #63

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/backend/router.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { queriesRouter } from '~/lib/mal/api/service.server';

import { t } from './trpc.server';

export const router = t.mergeRouters(queriesRouter);
3 changes: 3 additions & 0 deletions app/backend/trpc.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { initTRPC } from '@trpc/server';

export const t = initTRPC.create();
2 changes: 1 addition & 1 deletion app/components/my-list-status/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const STATUS_TO_ICONS: Record<NonNullable<MyListStatus['status']> | 'add', FC<{
};

export function MyListStatusPopup({ myListStatus }: MyListStatusProps) {
const { signedIn } = useRouteMatch<{ data: ClientAuthState }>('root').data;
const { data: { signedIn } = {} } = useRouteMatch<{ data: ClientAuthState }>('root');
if (!signedIn) return null;

const status = myListStatus?.status ?? 'add';
Expand Down
4 changes: 2 additions & 2 deletions app/components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function AppMenu({ signedIn }: ClientAuthState) {
}

export function Navigation() {
const { signedIn } = useRouteMatch<{ data: ClientAuthState }>('root').data;
const { data: { signedIn } = {} } = useRouteMatch<{ data: ClientAuthState }>('root');

return (
<nav className="bg-base-300">
Expand All @@ -87,7 +87,7 @@ export function Navigation() {
<SearchIcon className="w-5" />
</NavLink>
<ThemePicker />
<AppMenu signedIn={signedIn} />
<AppMenu signedIn={!!signedIn} />
</div>
</div>
</nav>
Expand Down
2 changes: 1 addition & 1 deletion app/components/theme-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function setThemeCookie(theme: Theme) {
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const { theme: defaultTheme } = useRouteMatch<{ data: ThemeClientState }>('root').data;
const { data: { theme: defaultTheme } = {} } = useRouteMatch<{ data?: ThemeClientState }>('root');

const [theme, setTheme] = useState<Theme>(defaultTheme);
const contextValue = useMemo(
Expand Down
27 changes: 27 additions & 0 deletions app/contracts/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod';

export const rankingTypeParamSchema = z.union([z.literal('all'), z.literal('airing'), z.literal('upcoming')]);
export type RankingTypeParam = z.infer<typeof rankingTypeParamSchema>;

export const seasonParamSchema = z.union([z.literal('winter'), z.literal('spring'), z.literal('summer'), z.literal('fall')]);
export type SeasonParam = z.infer<typeof seasonParamSchema>;

export const seasonalSortParamSchema = z.union([z.literal('anime_score'), z.literal('anime_num_list_users')]);
export type SeasonalSortQueryParam = z.infer<typeof seasonalSortParamSchema>;

export const myListStatusTypeSchema = z.union([
z.literal('completed'),
z.literal('plan_to_watch'),
z.literal('watching'),
z.literal('on_hold'),
z.literal('dropped'),
]);
export type MyListStatusType = z.infer<typeof myListStatusTypeSchema>;

export const myListSortParamSchema = z.union([
z.literal('list_score'),
z.literal('list_updated_at'),
z.literal('anime_title'),
z.literal('anime_start_date'),
]);
export type MyListSortQueryParam = z.infer<typeof myListSortParamSchema>;
282 changes: 198 additions & 84 deletions app/lib/mal/api/queries.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,211 @@
import { AuthState } from '~/contracts/auth';
import { z } from 'zod';

import { t } from '~/backend/trpc.server';
import { MyListNodeList, Node, NodeList } from '~/contracts/mal';
import {
MyListNodeList,
MyListSortQueryParam,
MyListStatus,
Node,
NodeList,
RankingTypeParam,
SeasonalSortQueryParam,
SeasonParam,
} from '~/contracts/mal';
myListSortParamSchema,
myListStatusTypeSchema,
rankingTypeParamSchema,
seasonalSortParamSchema,
seasonParamSchema,
} from '~/contracts/schemas';

import { malRequest } from './request';
type Limit = number;
type Offset = number;

const limitSchema = z.number();
const offsetSchema = z.number().optional();
const accessTokenSchema = z.string();

const LIST_FIELDS = 'id,title,main_picture,alternative_titles,mean,rank,popularity,media_type,status,start_season,num_episodes';
const DEFAULT_DETAIL_FIELDS =
'id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime{mean},recommendations{mean},studios,videos,statistics';

type AnimeList = {
q: string;
limit: Limit;
offset?: Offset;
};
export const animeList = (params: AnimeList) => {
return malRequest<NodeList>({
path: '/anime',
params: {
fields: LIST_FIELDS,
...params,
},
});
};
export const queriesRouter = t.router({
animeList: t.procedure
.input(
z.object({
q: z.string(),
limit: limitSchema,
offset: offsetSchema,
})
)
.query(({ input }) => {
return malRequest<NodeList>({
path: '/anime',
params: {
fields: LIST_FIELDS,
...input,
},
});
}),
animeDetail: t.procedure
.input(
z.object({
id: z.number(),
accessToken: z.optional(accessTokenSchema),
})
)
.query(({ input: { id, accessToken } }) => {
return malRequest<Node>({
path: `/anime/${id}`,
params: {
fields: accessToken
? `${DEFAULT_DETAIL_FIELDS},my_list_status{priority,comments,num_times_rewatched,rewatch_value}`
: DEFAULT_DETAIL_FIELDS,
},
accessToken,
});
}),
topAnime: t.procedure
.input(
z.object({
ranking_type: rankingTypeParamSchema,
limit: limitSchema,
offset: offsetSchema,
})
)
.query(({ input }) => {
return malRequest<NodeList>({
path: '/anime/ranking',
params: {
fields: LIST_FIELDS,
...input,
},
});
}),
seasonalAnime: t.procedure
.input(
z.object({
year: z.number(),
season: seasonParamSchema,
sort: seasonalSortParamSchema,
limit: limitSchema,
offset: offsetSchema,
})
)
.query(({ input: { season, year, ...params } }) => {
return malRequest<NodeList>({
path: `/anime/season/${year}/${season}`,
params: {
fields: LIST_FIELDS,
...params,
},
});
}),
myList: t.procedure
.input(
z.object({
status: myListStatusTypeSchema,
accessToken: accessTokenSchema,
sort: myListSortParamSchema,
limit: limitSchema,
offset: offsetSchema,
})
)
.query(({ input: { accessToken, ...params } }) => {
return malRequest<MyListNodeList>({
path: '/users/@me/animelist',
params: {
fields: `${LIST_FIELDS},list_status{start_date,finish_date}`,
...params,
},
accessToken,
});
}),
suggestedAnime: t.procedure
.input(
z.object({
accessToken: accessTokenSchema,
limit: limitSchema,
offset: offsetSchema,
})
)
.query(({ input: { accessToken, ...params } }) => {
return malRequest<NodeList>({
path: `/anime/suggestions`,
params: {
fields: LIST_FIELDS,
...params,
},
accessToken,
});
}),
});

// type AnimeList = {
// q: string;
// limit: Limit;
// offset?: Offset;
// };
// export const animeList = (params: AnimeList) => {
// return malRequest<NodeList>({
// path: '/anime',
// params: {
// fields: LIST_FIELDS,
// ...params,
// },
// });
// };

type AnimeDetail = { id: Node['id']; accessToken?: AuthState['accessToken'] };
export const animeDetail = ({ id, accessToken }: AnimeDetail) => {
return malRequest<Node>({
path: `/anime/${id}`,
params: {
fields: accessToken ? `${DEFAULT_DETAIL_FIELDS},my_list_status{priority,comments,num_times_rewatched,rewatch_value}` : DEFAULT_DETAIL_FIELDS,
},
accessToken,
});
};
// type AnimeDetail = { id: Node['id']; accessToken?: AuthState['accessToken'] };
// export const animeDetail = ({ id, accessToken }: AnimeDetail) => {
// return malRequest<Node>({
// path: `/anime/${id}`,
// params: {
// fields: accessToken ? `${DEFAULT_DETAIL_FIELDS},my_list_status{priority,comments,num_times_rewatched,rewatch_value}` : DEFAULT_DETAIL_FIELDS,
// },
// accessToken,
// });
// };

type TopAnime = { ranking_type: RankingTypeParam; limit: Limit; offset?: Offset };
export const topAnime = (params: TopAnime) => {
return malRequest<NodeList>({
path: '/anime/ranking',
params: {
fields: LIST_FIELDS,
...params,
},
});
};
// type TopAnime = { ranking_type: RankingTypeParam; limit: Limit; offset?: Offset };
// export const topAnime = (params: TopAnime) => {
// return malRequest<NodeList>({
// path: '/anime/ranking',
// params: {
// fields: LIST_FIELDS,
// ...params,
// },
// });
// };

type SeasonalAnime = { sort: SeasonalSortQueryParam; limit: Limit; offset?: Offset; season: SeasonParam; year: number };
export const seasonalAnime = ({ season, year, ...params }: SeasonalAnime) => {
return malRequest<NodeList>({
path: `/anime/season/${year}/${season}`,
params: {
fields: LIST_FIELDS,
...params,
},
});
};
// type SeasonalAnime = { sort: SeasonalSortQueryParam; limit: Limit; offset?: Offset; season: SeasonParam; year: number };
// export const seasonalAnime = ({ season, year, ...params }: SeasonalAnime) => {
// return malRequest<NodeList>({
// path: `/anime/season/${year}/${season}`,
// params: {
// fields: LIST_FIELDS,
// ...params,
// },
// });
// };

type MyList = {
status: MyListStatus['status'];
accessToken: AuthState['accessToken'];
sort: MyListSortQueryParam;
limit: Limit;
offset?: Offset;
};
export const myList = ({ accessToken, ...params }: MyList) => {
return malRequest<MyListNodeList>({
path: '/users/@me/animelist',
params: {
fields: `${LIST_FIELDS},list_status{start_date,finish_date}`,
...params,
},
accessToken,
});
};
// type MyList = {
// status: MyListStatus['status'];
// accessToken: AuthState['accessToken'];
// sort: MyListSortQueryParam;
// limit: Limit;
// offset?: Offset;
// };
// export const myList = ({ accessToken, ...params }: MyList) => {
// return malRequest<MyListNodeList>({
// path: '/users/@me/animelist',
// params: {
// fields: `${LIST_FIELDS},list_status{start_date,finish_date}`,
// ...params,
// },
// accessToken,
// });
// };

type SuggestedAnime = { limit: Limit; offset?: Offset; accessToken: AuthState['accessToken'] };
export const suggestedAnime = ({ accessToken, ...params }: SuggestedAnime) => {
return malRequest<NodeList>({
path: `/anime/suggestions`,
params: {
fields: LIST_FIELDS,
...params,
},
accessToken,
});
};
// type SuggestedAnime = { limit: Limit; offset?: Offset; accessToken: AuthState['accessToken'] };
// export const suggestedAnime = ({ accessToken, ...params }: SuggestedAnime) => {
// return malRequest<NodeList>({
// path: `/anime/suggestions`,
// params: {
// fields: LIST_FIELDS,
// ...params,
// },
// accessToken,
// });
// };
2 changes: 2 additions & 0 deletions app/lib/mal/api/service.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as mutate from './mutations';
import * as query from './queries';
export default { query, mutate };

export { queriesRouter } from './queries';
Loading