did a lot of syle work and started search and detail pages
This commit is contained in:
parent
7c5d2a25ff
commit
275fb87eeb
23 changed files with 301155 additions and 243 deletions
15
README.md
15
README.md
|
@ -1 +1,16 @@
|
|||
|
||||
|
||||
|
||||
# Notes
|
||||
|
||||
## APIS
|
||||
|
||||
### Generate openapi client
|
||||
|
||||
- path to source yml or json
|
||||
- path to output, will create a typescript file
|
||||
|
||||
example
|
||||
```bash
|
||||
bunx openapi-typescript .\src\features\content\apis\api.yml -o .\src\features\content\apis\api.generated.ts
|
||||
```
|
12
bun.lock
12
bun.lock
|
@ -7,6 +7,8 @@
|
|||
"@solid-primitives/context": "^0.3.1",
|
||||
"@solid-primitives/deep": "^0.3.2",
|
||||
"@solid-primitives/event-listener": "^2.4.1",
|
||||
"@solid-primitives/pagination": "^0.4.1",
|
||||
"@solid-primitives/scheduled": "^1.5.1",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.1.4",
|
||||
|
@ -348,6 +350,8 @@
|
|||
|
||||
"@solid-primitives/memo": ["@solid-primitives/memo@1.4.2", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.1", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-1w2MoD/25tZOImCI+dEL08n8dyyc6sg6o0zc14sZXBBa4XSz6TDuPYgQ24r+dQerXWoP6OgZ1VZz+Mo7c1Lmvg=="],
|
||||
|
||||
"@solid-primitives/pagination": ["@solid-primitives/pagination@0.4.1", "", { "dependencies": { "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Q/ZDa8qjKUCRW4Fvdunk8qlgf1geUAz5nfcJbI0sbIYf/9dhURMvESugpqFeF+/GJo784jNcfUwg5253/I7tOA=="],
|
||||
|
||||
"@solid-primitives/platform": ["@solid-primitives/platform@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg=="],
|
||||
|
||||
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA=="],
|
||||
|
@ -356,7 +360,7 @@
|
|||
|
||||
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-YJ+EveQeDv9DLqfDKfsPAAGy2x3vBruoD23yn+nD2dT84QjoBxWT1T0qA0TMFjek6/xuN3flqnHtQ4r++4zdjg=="],
|
||||
|
||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-RVw24IRNh1FQ4DCMb3OahB70tXIwc5vH8nhR4nNPsXwUPQeuOkLsDI5BlxaPk0vyZgqw9lDpufgI3HnPwplgDw=="],
|
||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-WKg/zvAyDIgQ/Xo48YaUY7ISaPyWTZNDzIVWP2R84CuLH+nZN/2O0aFn/gQlWY6y/Bfi/LdDt6Og2/PRzPY7mA=="],
|
||||
|
||||
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.0.8", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg=="],
|
||||
|
||||
|
@ -1722,10 +1726,14 @@
|
|||
|
||||
"@solid-devtools/debugger/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||
|
||||
"@solid-devtools/debugger/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-RVw24IRNh1FQ4DCMb3OahB70tXIwc5vH8nhR4nNPsXwUPQeuOkLsDI5BlxaPk0vyZgqw9lDpufgI3HnPwplgDw=="],
|
||||
|
||||
"@solid-devtools/debugger/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||
|
||||
"@solid-devtools/shared/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||
|
||||
"@solid-devtools/shared/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-RVw24IRNh1FQ4DCMb3OahB70tXIwc5vH8nhR4nNPsXwUPQeuOkLsDI5BlxaPk0vyZgqw9lDpufgI3HnPwplgDw=="],
|
||||
|
||||
"@solid-devtools/shared/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||
|
||||
"@solid-primitives/bounds/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||
|
@ -1744,8 +1752,6 @@
|
|||
|
||||
"@solid-primitives/media/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||
|
||||
"@solid-primitives/memo/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-WKg/zvAyDIgQ/Xo48YaUY7ISaPyWTZNDzIVWP2R84CuLH+nZN/2O0aFn/gQlWY6y/Bfi/LdDt6Og2/PRzPY7mA=="],
|
||||
|
||||
"@solid-primitives/refs/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||
|
||||
"@solid-primitives/resize-observer/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
"@solid-primitives/context": "^0.3.1",
|
||||
"@solid-primitives/deep": "^0.3.2",
|
||||
"@solid-primitives/event-listener": "^2.4.1",
|
||||
"@solid-primitives/pagination": "^0.4.1",
|
||||
"@solid-primitives/scheduled": "^1.5.1",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.1.4",
|
||||
|
|
31
src/components/details/details.module.css
Normal file
31
src/components/details/details.module.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
.container {
|
||||
isolation: isolate;
|
||||
display: block grid;
|
||||
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
block-size: 80cqb;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: block;
|
||||
background: linear-gradient(182.5deg, transparent 20%, var(--surface-2) 90%),
|
||||
linear-gradient(transparent 50%, #0007 75%);
|
||||
}
|
||||
|
||||
& > .background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
19
src/components/details/details.tsx
Normal file
19
src/components/details/details.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Component } from 'solid-js';
|
||||
import { Entry } from '~/features/content';
|
||||
import css from './details.module.css';
|
||||
|
||||
interface DetailsProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export const Details: Component<DetailsProps> = (props) => {
|
||||
return (
|
||||
<div class={css.container}>
|
||||
<header class={css.header}>
|
||||
<img class={css.background} src={props.entry.image} />
|
||||
|
||||
<h1>{props.entry.title}</h1>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
};
|
3
src/components/details/index.ts
Normal file
3
src/components/details/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
export { Details } from './details';
|
|
@ -29,7 +29,7 @@
|
|||
gap: 1rem;
|
||||
justify-content: start;
|
||||
|
||||
padding-inline: 2rem;
|
||||
padding-inline: var(--size-6);
|
||||
|
||||
inline-size: 100%;
|
||||
block-size: 8.333333em;
|
||||
|
@ -53,7 +53,7 @@
|
|||
align-content: end;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
padding: var(--size-6);
|
||||
block-size: 80vh;
|
||||
overflow: clip;
|
||||
container-type: scroll-state;
|
||||
|
@ -114,6 +114,11 @@
|
|||
text-decoration-color: var(--gray-8);
|
||||
padding: var(--size-3);
|
||||
font-weight: var(--font-weight-9);
|
||||
outline-offset: var(--size-1);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--gray-2);
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
|
@ -152,9 +157,6 @@
|
|||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
/* 80% {
|
||||
opacity: 0;
|
||||
} */
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.container {
|
||||
--_space: var(--size-6);
|
||||
display: grid;
|
||||
grid: auto auto / auto auto;
|
||||
grid-template-areas:
|
||||
|
@ -6,16 +7,21 @@
|
|||
"list list";
|
||||
justify-content: space-between;
|
||||
inline-size: 100%;
|
||||
|
||||
padding-inline: var(--_space);
|
||||
}
|
||||
|
||||
.heading {
|
||||
grid-area: heading;
|
||||
font-size: 2em;
|
||||
font-size: var(--size-7);
|
||||
color: var(--text-1);
|
||||
|
||||
padding-inline: var(--_space);
|
||||
}
|
||||
|
||||
.metadata {
|
||||
grid-area: metadata;
|
||||
opacity: 0.6;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.list {
|
||||
|
@ -26,10 +32,10 @@
|
|||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
|
||||
gap: 2em;
|
||||
padding: 12em 4em 5em;
|
||||
scroll-padding: 4em;
|
||||
margin: -10em -4em 0em;
|
||||
gap: var(--_space);
|
||||
padding: calc(8 * var(--_space)) calc(2 * var(--_space)) calc(2.5 * var(--_space));
|
||||
scroll-padding: calc(2 * var(--_space));
|
||||
margin: calc(-7 * var(--_space)) calc(-1 * var(--_space)) 0em;
|
||||
|
||||
overflow: visible auto;
|
||||
scroll-snap-type: inline mandatory;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use server";
|
||||
|
||||
import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript
|
||||
import createClient from "openapi-fetch";
|
||||
import { query } from "@solidjs/router";
|
||||
|
@ -20,42 +18,29 @@ type ItemImageType =
|
|||
| "BoxRear"
|
||||
| "Profile";
|
||||
|
||||
const baseUrl = process.env.JELLYFIN_BASE_URL;
|
||||
const client = createClient<paths>({
|
||||
baseUrl,
|
||||
const getBaseUrl = () => {
|
||||
"use server";
|
||||
|
||||
return process.env.JELLYFIN_BASE_URL;
|
||||
};
|
||||
|
||||
|
||||
const getClient = () => {
|
||||
"use server";
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl: getBaseUrl(),
|
||||
headers: {
|
||||
Authorization: `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
||||
"Content-Type": 'application/json; profile="CamelCase"',
|
||||
},
|
||||
});
|
||||
|
||||
export const TEST = query(async () => {
|
||||
const userId = "a9c51af8-4bf5-4578-a99a-b4dd0ebf0763";
|
||||
const itemId = "919dfa97-e4da-d275-8a92-5d056e590a28";
|
||||
const seriesId = "5230ddbcd-9400-733d-c07e-5b8cb7a4f49";
|
||||
|
||||
const { data: seriesData } = await client.GET(
|
||||
"/UserItems/{itemId}/UserData",
|
||||
{
|
||||
params: {
|
||||
path: { itemId: seriesId },
|
||||
query: { userId },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { data: epData } = await client.GET("/UserItems/{itemId}/UserData", {
|
||||
params: {
|
||||
path: { itemId },
|
||||
query: { userId },
|
||||
},
|
||||
});
|
||||
|
||||
console.log(seriesData, epData);
|
||||
}, "jellyfin.TEST");
|
||||
})
|
||||
};
|
||||
|
||||
export const getCurrentUser = query(async () => {
|
||||
const { data, error, response } = await client.GET("/Users/Public", {
|
||||
"use server";
|
||||
|
||||
const { data, error, response } = await getClient().GET("/Users/Public", {
|
||||
params: {},
|
||||
});
|
||||
|
||||
|
@ -63,7 +48,9 @@ export const getCurrentUser = query(async () => {
|
|||
}, "jellyfin.getCurrentUser");
|
||||
|
||||
export const listUsers = query(async () => {
|
||||
const { data, error } = await client.GET("/Users", {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Users", {
|
||||
params: {},
|
||||
});
|
||||
|
||||
|
@ -72,7 +59,9 @@ export const listUsers = query(async () => {
|
|||
|
||||
export const listItems = query(
|
||||
async (userId: string): Promise<Entry[] | undefined> => {
|
||||
const { data, error } = await client.GET("/Items", {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Items", {
|
||||
params: {
|
||||
query: {
|
||||
userId,
|
||||
|
@ -99,7 +88,7 @@ export const listItems = query(
|
|||
// id: item.Id!,
|
||||
id: item.ProviderIds!["Tmdb"]!,
|
||||
title: item.Name!,
|
||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||
})) ?? []
|
||||
);
|
||||
},
|
||||
|
@ -107,14 +96,19 @@ export const listItems = query(
|
|||
);
|
||||
|
||||
export const getRandomItem = query(
|
||||
async (userId: string): Promise<Entry | undefined> =>
|
||||
getRandomItems(userId, 1).then((items) => items?.at(0)),
|
||||
async (userId: string): Promise<Entry | undefined> => {
|
||||
"use server";
|
||||
|
||||
return getRandomItems(userId, 1).then((items) => items?.at(0));
|
||||
},
|
||||
"jellyfin.listRandomItem",
|
||||
);
|
||||
|
||||
export const getRandomItems = query(
|
||||
async (userId: string, limit: number = 10): Promise<Entry[]> => {
|
||||
const { data, error } = await client.GET("/Items", {
|
||||
async (userId: string, limit: number = 20): Promise<Entry[]> => {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Items", {
|
||||
params: {
|
||||
query: {
|
||||
userId,
|
||||
|
@ -140,8 +134,8 @@ export const getRandomItems = query(
|
|||
// id: item.Id!,
|
||||
id: item.ProviderIds!["Tmdb"]!,
|
||||
title: item.Name!,
|
||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||
image: new URL(`/Items/${item.Id!}/Images/Backdrop`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||
image: new URL(`/Items/${item.Id!}/Images/Backdrop`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||
})) ?? []
|
||||
);
|
||||
},
|
||||
|
@ -150,7 +144,9 @@ export const getRandomItems = query(
|
|||
|
||||
export const getItem = query(
|
||||
async (userId: string, itemId: string): Promise<Entry | undefined> => {
|
||||
const { data, error } = await client.GET("/Items/{itemId}", {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Items/{itemId}", {
|
||||
params: {
|
||||
path: {
|
||||
itemId,
|
||||
|
@ -180,8 +176,8 @@ export const getItem = query(
|
|||
id: data.ProviderIds!["Tmdb"]!,
|
||||
title: data.Name!,
|
||||
overview: data.Overview!,
|
||||
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||
image: new URL(`/Items/${itemId}/Images/Backdrop`, baseUrl),
|
||||
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||
image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()),
|
||||
// ...data,
|
||||
};
|
||||
},
|
||||
|
@ -193,7 +189,9 @@ export const getItemImage = query(
|
|||
itemId: string,
|
||||
imageType: ItemImageType,
|
||||
): Promise<any | undefined> => {
|
||||
const { data, error } = await client.GET(
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET(
|
||||
"/Items/{itemId}/Images/{imageType}",
|
||||
{
|
||||
parseAs: "blob",
|
||||
|
@ -214,7 +212,9 @@ export const getItemImage = query(
|
|||
|
||||
export const getItemPlaybackInfo = query(
|
||||
async (userId: string, itemId: string): Promise<any | undefined> => {
|
||||
const { data, error, response } = await client.GET(
|
||||
"use server";
|
||||
|
||||
const { data, error, response } = await getClient().GET(
|
||||
"/Items/{itemId}/PlaybackInfo",
|
||||
{
|
||||
parseAs: "text",
|
||||
|
@ -236,7 +236,9 @@ export const getItemPlaybackInfo = query(
|
|||
);
|
||||
|
||||
export const queryItems = query(async () => {
|
||||
const { data, error } = await client.GET("/Items", {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/Items", {
|
||||
params: {
|
||||
query: {
|
||||
mediaTypes: ["Video"],
|
||||
|
@ -254,7 +256,9 @@ export const queryItems = query(async () => {
|
|||
|
||||
export const getContinueWatching = query(
|
||||
async (userId: string): Promise<Entry[]> => {
|
||||
const { data, error } = await client.GET("/UserItems/Resume", {
|
||||
"use server";
|
||||
|
||||
const { data, error } = await getClient().GET("/UserItems/Resume", {
|
||||
params: {
|
||||
query: {
|
||||
userId,
|
||||
|
|
106409
src/features/content/apis/tmdb.generated.ts
Normal file
106409
src/features/content/apis/tmdb.generated.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
export interface paths {
|
||||
"/4/account/{account_object_id}/movie/recommendations": {
|
||||
"/account/{account_object_id}/movie/recommendations": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
@ -15,70 +15,6 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/3/movie/{movie_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["GetMovieById"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/3/series/{series_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["GetSeriesById"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/3/discover/movie": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["GetDiscovery_Movie"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/3/discover/tv": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["GetDiscovery_Serie"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
|
|
|
@ -1,25 +1,42 @@
|
|||
"use server";
|
||||
|
||||
import createClient from "openapi-fetch";
|
||||
import { query } from "@solidjs/router";
|
||||
import { Entry } from "../types";
|
||||
import { paths } from "./tmdb.not.generated";
|
||||
import { Entry, SearchResult } from "../types";
|
||||
import { paths as pathsV3 } from "./tmdb.generated";
|
||||
import { paths as pathsV4 } from "./tmdb.not.generated";
|
||||
|
||||
const getClients = () => {
|
||||
"use server";
|
||||
|
||||
const baseUrl = process.env.TMDB_BASE_URL;
|
||||
const client = createClient<paths>({
|
||||
baseUrl,
|
||||
const clientV3 = createClient<pathsV3>({
|
||||
baseUrl: `${baseUrl}/3`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.TMDB_TOKEN}`,
|
||||
"Content-Type": "application/json;",
|
||||
},
|
||||
});
|
||||
|
||||
const clientV4 = createClient<pathsV4>({
|
||||
baseUrl: `${baseUrl}/4`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.TMDB_TOKEN}`,
|
||||
"Content-Type": "application/json;",
|
||||
},
|
||||
});
|
||||
|
||||
return [clientV3, clientV4] as const;
|
||||
};
|
||||
|
||||
export const getEntry = query(
|
||||
async (id: string): Promise<Entry | undefined> => {
|
||||
const { data } = await client.GET("/3/movie/{movie_id}", {
|
||||
"use server";
|
||||
|
||||
const [ clientV3 ] = getClients();
|
||||
|
||||
const { data } = await clientV3.GET("/movie/{movie_id}", {
|
||||
params: {
|
||||
path: {
|
||||
movie_id: id,
|
||||
movie_id: Number.parseInt(id),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -29,8 +46,8 @@ export const getEntry = query(
|
|||
}
|
||||
|
||||
return {
|
||||
id: String(data.id),
|
||||
title: data.title,
|
||||
id: String(data.id ?? -1),
|
||||
title: data.title!,
|
||||
overview: data.overview,
|
||||
thumbnail: `http://image.tmdb.org/t/p/w342${data.poster_path}`,
|
||||
image: `http://image.tmdb.org/t/p/original${data.backdrop_path}`,
|
||||
|
@ -40,10 +57,14 @@ export const getEntry = query(
|
|||
);
|
||||
|
||||
export const getRecommendations = query(async (): Promise<Entry[]> => {
|
||||
"use server";
|
||||
|
||||
const [ ,clientV4 ] = getClients();
|
||||
|
||||
const account_object_id = "6668b76e419b28ec1a1c5aab";
|
||||
|
||||
const { data } = await client.GET(
|
||||
"/4/account/{account_object_id}/movie/recommendations",
|
||||
const { data } = await clientV4.GET(
|
||||
"/account/{account_object_id}/movie/recommendations",
|
||||
{
|
||||
params: {
|
||||
path: { account_object_id },
|
||||
|
@ -57,8 +78,8 @@ export const getRecommendations = query(async (): Promise<Entry[]> => {
|
|||
|
||||
return data?.results.map(
|
||||
({ id, title, overview, poster_path, backdrop_path }) => ({
|
||||
id: String(id),
|
||||
title,
|
||||
id: String(id ?? -1),
|
||||
title: title!,
|
||||
overview,
|
||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||
|
@ -67,25 +88,71 @@ export const getRecommendations = query(async (): Promise<Entry[]> => {
|
|||
}, "tmdb.getRecommendations");
|
||||
|
||||
export const getDiscovery = query(async (): Promise<Entry[]> => {
|
||||
"use server";
|
||||
|
||||
const [ clientV3 ] = getClients();
|
||||
|
||||
const [{ data: movies }, { data: series }] = await Promise.all([
|
||||
client.GET("/3/discover/movie"),
|
||||
client.GET("/3/discover/movie"),
|
||||
clientV3.GET("/discover/movie"),
|
||||
clientV3.GET("/discover/tv"),
|
||||
]);
|
||||
|
||||
if (movies === undefined || series === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// console.log({ movies: movies.results.length, series: series.results.length });
|
||||
|
||||
return movies?.results
|
||||
.slice(0, 9)
|
||||
.concat(series?.results.slice(0, 9))
|
||||
const movieEntries = movies?.results?.slice(0, 10)
|
||||
.map(({ id, title, overview, poster_path, backdrop_path }) => ({
|
||||
id: String(id),
|
||||
title,
|
||||
id: String(id ?? -1),
|
||||
title: title!,
|
||||
overview,
|
||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||
}));
|
||||
})) ?? []
|
||||
|
||||
const seriesEntries = series?.results?.slice(0, 10)
|
||||
.map(({ id, name, overview, poster_path, backdrop_path }) => ({
|
||||
id: String(id ?? -1),
|
||||
title: name!,
|
||||
overview,
|
||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||
})) ?? []
|
||||
|
||||
return movieEntries.concat(seriesEntries);
|
||||
}, "tmdb.getDiscovery");
|
||||
|
||||
export const searchMulti = query(async (query: string, page: number = 1): Promise<SearchResult> => {
|
||||
"use server";
|
||||
|
||||
if (query.length === 0) {
|
||||
return { count: 0, pages: 0, results: [] };
|
||||
}
|
||||
const [ clientV3 ] = getClients();
|
||||
|
||||
|
||||
const { data } = await clientV3.GET("/search/multi", {
|
||||
params: {
|
||||
query: {
|
||||
query,
|
||||
page,
|
||||
include_adult: false,
|
||||
language: 'en-US'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data === undefined) {
|
||||
return { count: 0, pages: 0, results: [] };
|
||||
}
|
||||
|
||||
console.log(`loaded page ${page}, found ${data.results?.length} results`);
|
||||
|
||||
return { count: data.total_results!, pages: data.total_pages!, results: data.results?.map(({ id, name, title, media_type, overview, backdrop_path, poster_path }) => ({
|
||||
id: String(id),
|
||||
title: `${name ?? title ?? ''} (${media_type})`,
|
||||
overview,
|
||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||
})) ?? [] };
|
||||
}, "tmdb.search.multi");
|
194262
src/features/content/apis/tmdb.yml
Normal file
194262
src/features/content/apis/tmdb.yml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,12 +2,12 @@
|
|||
|
||||
import type { Category, Entry } from "./types";
|
||||
import { query } from "@solidjs/router";
|
||||
import { entries } from "./data";
|
||||
import { getContinueWatching, getItem, getRandomItems } from "./apis/jellyfin";
|
||||
import { getContinueWatching, getRandomItems } from "./apis/jellyfin";
|
||||
import {
|
||||
getDiscovery,
|
||||
getRecommendations,
|
||||
getEntry as getTmdbEntry,
|
||||
searchMulti,
|
||||
} from "./apis/tmdb";
|
||||
|
||||
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||
|
@ -19,20 +19,25 @@ export const listCategories = query(async (): Promise<Category[]> => {
|
|||
return [
|
||||
// { label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
|
||||
{
|
||||
label: "Recommendations (For you?)",
|
||||
label: "For you",
|
||||
entries: await getRecommendations(),
|
||||
},
|
||||
{ label: "Discover", entries: await getDiscovery() },
|
||||
{ label: "Random", entries: await getRandomItems(jellyfinUserId) },
|
||||
];
|
||||
}, "series.categories.list");
|
||||
}, "content.categories.list");
|
||||
|
||||
export const getEntry = query(
|
||||
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
||||
return getTmdbEntry(id);
|
||||
// return getItem(jellyfinUserId, id);
|
||||
},
|
||||
"series.get",
|
||||
"content.get",
|
||||
);
|
||||
|
||||
export const search = query(async (query: string, page: number = 1) => {
|
||||
"use server";
|
||||
return searchMulti(query, page);
|
||||
}, 'content.search');
|
||||
|
||||
export { listUsers, getContinueWatching, listItems } from "./apis/jellyfin";
|
||||
|
|
|
@ -29,3 +29,9 @@ export namespace Entry {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
count: number;
|
||||
pages: number;
|
||||
results: Entry[];
|
||||
}
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
gap: 2em;
|
||||
border-radius: inherit;
|
||||
|
||||
& > .hero {
|
||||
gap: var(--size-6);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
& > .list {
|
||||
padding-inline: 4em;
|
||||
}
|
||||
}
|
|
@ -3,10 +3,10 @@ import {
|
|||
createEventSignal,
|
||||
} from "@solid-primitives/event-listener";
|
||||
import { createAsync, json, query } from "@solidjs/router";
|
||||
import { Component, createEffect, createMemo, createSignal } from "solid-js";
|
||||
import { Component, createEffect, createMemo, createSignal, on } from "solid-js";
|
||||
import css from "./player.module.css";
|
||||
import { Volume } from "./controls/volume";
|
||||
import { getEntry } from "../content";
|
||||
import { Entry, getEntry } from "../content";
|
||||
|
||||
const metadata = query(async (id: string) => {
|
||||
"use server";
|
||||
|
@ -36,7 +36,7 @@ const metadata = query(async (id: string) => {
|
|||
}, "player.metadata");
|
||||
|
||||
interface PlayerProps {
|
||||
id: string;
|
||||
entry: Entry;
|
||||
}
|
||||
|
||||
export const Player: Component<PlayerProps> = (props) => {
|
||||
|
@ -44,9 +44,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
undefined as unknown as HTMLVideoElement,
|
||||
);
|
||||
|
||||
const entry = createAsync(() => getEntry(props.id));
|
||||
|
||||
const data = createAsync(() => metadata(props.id), {
|
||||
const data = createAsync(() => metadata(props.entry.id), {
|
||||
deferStream: true,
|
||||
initialValue: {} as any,
|
||||
});
|
||||
|
@ -65,25 +63,12 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
: "";
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const metadata = data();
|
||||
const el = video();
|
||||
|
||||
if (metadata === undefined || el === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(metadata);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
thumbnails();
|
||||
|
||||
console.log(video()!.textTracks.getTrackById("thumbnails")?.cues);
|
||||
createEffect(on(thumbnails, (thumbnails) => {
|
||||
// console.log(thumbnails, video()!.textTracks.getTrackById("thumbnails")?.cues);
|
||||
|
||||
// const captions = el.addTextTrack("captions", "English", "en");
|
||||
// captions.
|
||||
});
|
||||
}));
|
||||
|
||||
const onDurationChange = createEventSignal(video, "durationchange");
|
||||
const onTimeUpdate = createEventSignal(video, "timeupdate");
|
||||
|
@ -102,53 +87,53 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
|
||||
createEventListenerMap(() => video()!, {
|
||||
durationchange(e) {
|
||||
console.log("durationchange", e);
|
||||
// console.log("durationchange", e);
|
||||
},
|
||||
loadeddata(e) {
|
||||
console.log("loadeddata", e);
|
||||
// console.log("loadeddata", e);
|
||||
},
|
||||
loadedmetadata(e) {
|
||||
console.log("loadedmetadata", e);
|
||||
// console.log("loadedmetadata", e);
|
||||
},
|
||||
ratechange(e) {
|
||||
console.log("ratechange", e);
|
||||
// console.log("ratechange", e);
|
||||
},
|
||||
seeked(e) {
|
||||
console.log("seeked", e);
|
||||
// console.log("seeked", e);
|
||||
},
|
||||
seeking(e) {
|
||||
console.log("seeking", e);
|
||||
// console.log("seeking", e);
|
||||
},
|
||||
stalled(e) {
|
||||
console.log(
|
||||
"stalled (meaning downloading data failed)",
|
||||
e,
|
||||
video()!.error,
|
||||
);
|
||||
// console.log(
|
||||
// "stalled (meaning downloading data failed)",
|
||||
// e,
|
||||
// video()!.error,
|
||||
// );
|
||||
},
|
||||
|
||||
play(e) {
|
||||
console.log("play", e);
|
||||
// console.log("play", e);
|
||||
},
|
||||
canplay(e) {
|
||||
console.log("canplay", e);
|
||||
// console.log("canplay", e);
|
||||
},
|
||||
playing(e) {
|
||||
console.log("playing", e);
|
||||
// console.log("playing", e);
|
||||
},
|
||||
pause(e) {
|
||||
console.log("pause", e);
|
||||
// console.log("pause", e);
|
||||
},
|
||||
suspend(e) {
|
||||
// console.log("suspend", e);
|
||||
},
|
||||
|
||||
volumechange(e) {
|
||||
console.log("volumechange", e);
|
||||
// console.log("volumechange", e);
|
||||
},
|
||||
|
||||
waiting(e) {
|
||||
console.log("waiting", e);
|
||||
// console.log("waiting", e);
|
||||
},
|
||||
|
||||
progress(e) {
|
||||
|
@ -172,7 +157,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
|
||||
return (
|
||||
<figure class={css.player}>
|
||||
<h1>{entry()?.title}</h1>
|
||||
<h1>{props.entry?.title}</h1>
|
||||
|
||||
<video
|
||||
ref={setVideo}
|
||||
|
|
|
@ -26,15 +26,12 @@
|
|||
content: "";
|
||||
position: absolute;
|
||||
inset-inline-start: 100%;
|
||||
inset-block: 0;
|
||||
inline-size: 20vw;
|
||||
/* background:
|
||||
radial-gradient(ellipse at left center 100% 100%, #f00, transparent),
|
||||
linear-gradient(to right, #0003, transparent); */
|
||||
background-image: linear-gradient(to right, #0003, transparent);
|
||||
inset-block: -1em;
|
||||
inline-size: 40vw;
|
||||
background-image: linear-gradient(to right, rgb(from var(--surface-1) r g b / .9) 50%, transparent);
|
||||
mask: radial-gradient(
|
||||
ellipse 20vw 100% at left center,
|
||||
black,
|
||||
ellipse 40vw 100% at left center,
|
||||
black 25%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(5px);
|
||||
|
@ -53,24 +50,24 @@
|
|||
transition:
|
||||
transform 2s var(--ease-spring-5),
|
||||
opacity 0.3s var(--ease-3);
|
||||
color: var(--stone-4);
|
||||
color: var(--text-2);
|
||||
font-size: 2rem;
|
||||
line-height: 1.5;
|
||||
|
||||
& > span {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s var(--ease-3);
|
||||
text-shadow: 0 0 1em #000;
|
||||
text-shadow: 0 0 .5em var(--surface-1);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
fill: var(--stone-4);
|
||||
fill: var(--text-2);
|
||||
inline-size: 2.5rem;
|
||||
block-size: 2.5rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--yellow-4);
|
||||
color: var(--yellow-5);
|
||||
list-style: disc;
|
||||
|
||||
&::before {
|
||||
|
@ -80,28 +77,28 @@
|
|||
}
|
||||
|
||||
& > svg {
|
||||
fill: var(--yellow-4);
|
||||
fill: var(--yellow-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(a:is(:hover, :focus))::before {
|
||||
&:has(a:is(:hover, :focus)) {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:has(a:is(:hover, :focus)) > a {
|
||||
transform: scale(
|
||||
max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index)))))
|
||||
);
|
||||
& > a {
|
||||
transform: scale(max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index))))));
|
||||
|
||||
& > span {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:has(a:is(:hover, :focus):nth-child(1)) {
|
||||
--target: 1;
|
||||
|
|
|
@ -67,10 +67,10 @@ const [ThemeContextProvider, useTheme] = createContextProvider<
|
|||
},
|
||||
|
||||
setColorScheme(colorScheme) {
|
||||
// updateState({ colorScheme, hue: state.latest!.hue });
|
||||
updateState({ colorScheme, hue: state.latest!.hue });
|
||||
},
|
||||
setHue(hue) {
|
||||
// updateState({ hue, colorScheme: state.latest!.colorScheme });
|
||||
updateState({ hue, colorScheme: state.latest!.colorScheme });
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
59
src/routes/(shell)/details/[slug].tsx
Normal file
59
src/routes/(shell)/details/[slug].tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
createAsync,
|
||||
json,
|
||||
Params,
|
||||
query,
|
||||
redirect,
|
||||
RouteDefinition,
|
||||
useParams,
|
||||
} from "@solidjs/router";
|
||||
import { Show } from "solid-js";
|
||||
import { Details } from "~/components/details";
|
||||
import { createSlug, Entry, getEntry } from "~/features/content";
|
||||
|
||||
const healUrl = async (slug: string, entry: Entry) => {
|
||||
const actualSlug = createSlug(entry);
|
||||
|
||||
if (slug !== actualSlug) {
|
||||
// Not entirely sure a permanent redirect is what we want in this case
|
||||
throw redirect(`/details/${actualSlug}`, { status: 308 });
|
||||
}
|
||||
};
|
||||
|
||||
interface ItemParams extends Params {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export const route = {
|
||||
async preload({ params }) {
|
||||
const slug = params.slug;
|
||||
|
||||
if (!slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
|
||||
|
||||
if (entry === undefined) {
|
||||
return json(null, { status: 404 });
|
||||
}
|
||||
|
||||
healUrl(slug, entry);
|
||||
|
||||
return entry;
|
||||
},
|
||||
} satisfies RouteDefinition;
|
||||
|
||||
export default function Item() {
|
||||
const { slug } = useParams<ItemParams>();
|
||||
const id = slug.slice(slug.lastIndexOf("-") + 1);
|
||||
const entry = createAsync(() => getEntry(id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
|
||||
entry => <Details entry={entry()} />
|
||||
}</Show>
|
||||
</>
|
||||
);
|
||||
}
|
46
src/routes/(shell)/search/index.module.css
Normal file
46
src/routes/(shell)/search/index.module.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
.container {
|
||||
display: block grid;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 100%;
|
||||
|
||||
padding: var(--size-7);
|
||||
gap: var(--size-7);
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
|
||||
padding: var(--size-7);
|
||||
padding-block-end: var(--size-2);
|
||||
margin: calc(-1 * var(--size-7));
|
||||
margin-block-end: calc(-1 * var(--size-2));
|
||||
background-color: var(--surface-2);
|
||||
}
|
||||
|
||||
.grid {
|
||||
inline-size: 100%;
|
||||
display: block grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: var(--size-6);
|
||||
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
& > .item {
|
||||
inline-size: 100%;
|
||||
display: block grid;
|
||||
grid: 100% / 100%;
|
||||
place-items: center;
|
||||
|
||||
padding: 0;
|
||||
background-color: var(--surface-3);
|
||||
border-radius: var(--size-2);
|
||||
|
||||
aspect-ratio: 3 / 5;
|
||||
}
|
||||
|
||||
& > svg {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,67 @@
|
|||
import { createInfiniteScroll } from "@solid-primitives/pagination";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { createEffect, createSignal, For, on, onMount, Show, createComputed, batch, createMemo, untrack } from "solid-js";
|
||||
import { createSlug, search } from "~/features/content";
|
||||
import { AiOutlineLoading } from "solid-icons/ai";
|
||||
import css from './index.module.css';
|
||||
import { debounce } from "@solid-primitives/scheduled";
|
||||
|
||||
const getResults = async (query: string, page: number) => {
|
||||
const { results } = await search(query, page + 1);
|
||||
return results;
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const [ query, setQuery ] = createSignal(""); // lord of the rings
|
||||
const [ ref, setRef ] = createSignal<HTMLInputElement>();
|
||||
|
||||
const KAAS = createMemo(() => {
|
||||
const q = query();
|
||||
const [pages, setEl, { end }] = createInfiniteScroll((page) => getResults(q, page));
|
||||
|
||||
return { pages, setEl, end };
|
||||
});
|
||||
// const result = createAsync(() => search(query()), { initialValue: { count: 0, pages: 0, results: [] } });
|
||||
|
||||
const title = 'Search';
|
||||
return <>
|
||||
|
||||
createEffect(() => {
|
||||
KAAS();
|
||||
|
||||
untrack(ref)?.focus();
|
||||
});
|
||||
|
||||
return <div class={css.container}>
|
||||
<Title>{title}</Title>
|
||||
<h1>{title}</h1>
|
||||
</>;
|
||||
|
||||
<header class={css.header}>
|
||||
<input ref={setRef} type="search" placeholder={title} value={query()} oninput={debounce(e => setQuery(e.target.value), 300)} />
|
||||
</header>
|
||||
|
||||
<ul class={css.grid}>
|
||||
<For each={KAAS().pages()}>{
|
||||
item => <a id={`item:${item.id}`} href={`/details/${createSlug(item)}`}>
|
||||
<img class={css.item} src={item.thumbnail} title={item.title} />
|
||||
</a>
|
||||
}</For>
|
||||
|
||||
<Show when={!KAAS().end()}>
|
||||
<AiOutlineLoading ref={KAAS().setEl} />
|
||||
</Show>
|
||||
|
||||
<Show when={KAAS().pages().length === 0}>
|
||||
<p>No results</p>
|
||||
</Show>
|
||||
</ul>
|
||||
|
||||
{/* <output>
|
||||
<p>{result().count}</p>
|
||||
|
||||
<ul>
|
||||
<For each={result().results}>{
|
||||
result => <li>{result.title}</li>
|
||||
}</For>
|
||||
</ul>
|
||||
</output> */}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
createAsync,
|
||||
json,
|
||||
Params,
|
||||
query,
|
||||
|
@ -47,10 +48,11 @@ export const route = {
|
|||
export default function Item() {
|
||||
const { slug } = useParams<ItemParams>();
|
||||
const id = slug.slice(slug.lastIndexOf("-") + 1);
|
||||
const entry = createAsync(() => getEntry(id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Player id={id} />
|
||||
<Player entry={entry} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue