started on search page
continuous-integration/drone/push Build is passing Details

nextui
Guus van Meerveld 8 months ago
parent da37c946c4
commit 7438d26fa8

@ -19,6 +19,7 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",
"use-debounce": "^10.0.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {

@ -31,7 +31,7 @@ export const VideoCard: Component<{ data: TrendingVideo }> = ({
{video.title} {video.title}
</p> </p>
<div className="flex flex-row gap-2 justify-start overflow-scroll"> <div className="flex flex-row gap-2 justify-start overflow-scroll">
<p className="text-small tracking-tight text-default-400"> <p className="text-small font-semibold tracking-tight text-default-400">
{video.author.name} {video.author.name}
</p> </p>
<p className="text-small tracking-tight text-default-400"> <p className="text-small tracking-tight text-default-400">

@ -0,0 +1,17 @@
"use client";
import { Search } from "@/components/Search";
import { Component } from "@/typings/component";
import { useSearchParams } from "next/navigation";
export const SearchPage: Component = () => {
const searchParams = useSearchParams();
const query = searchParams.get("search_query");
return (
<>
<Search initialQueryValue={query || undefined} />
</>
);
};

@ -0,0 +1,12 @@
import { Suspense } from "react";
import { SearchPage } from "./SearchPage";
export default function Page() {
return (
<>
<Suspense>
<SearchPage />
</Suspense>
</>
);
}

@ -0,0 +1,12 @@
"use client";
import { Component } from "@/typings/component";
import { useSearchParams } from "next/navigation";
export const Watch: Component = () => {
const searchParams = useSearchParams();
const videoId = searchParams.get("v");
return <></>;
};

@ -0,0 +1,12 @@
import { Suspense } from "react";
import { Watch } from "./Watch";
export default function Page() {
return (
<>
<Suspense>
<Watch />
</Suspense>
</>
);
}

@ -1,7 +1,10 @@
import { TrendingVideo } from "../typings/trending"; import { Suggestions } from "@/client/typings/search/suggestions";
import { TrendingVideo } from "@/client/typings/trending";
export interface ConnectedAdapter { export interface ConnectedAdapter {
getTrending(region: string): Promise<TrendingVideo[]>; getTrending(region: string): Promise<TrendingVideo[]>;
getSearchSuggestions(query: string): Promise<Suggestions>;
} }
export default interface Adapter { export default interface Adapter {

@ -1,14 +1,20 @@
import ky from "ky"; import ky from "ky";
import Trending, { TrendingModel } from "./typings/trending"; import Trending, { TrendingModel } from "./typings/trending";
import Suggestions, { SuggestionsModel } from "./typings/search/suggestions";
import Adapter, { ApiType } from "@/client/adapters"; import Adapter, { ApiType } from "@/client/adapters";
import Transformer from "./transformer"; import Transformer from "./transformer";
const apiPath = (path: string): string => `/api/v1/${path}`; import path from "path";
const apiPath = (...paths: string[]): string =>
path.join("api", "v1", ...paths);
export type TrendingVideoType = "music" | "gaming" | "news" | "movies"; export type TrendingVideoType = "music" | "gaming" | "news" | "movies";
export const getTrending = async ( const getTrending = async (
baseUrl: string, baseUrl: string,
region?: string, region?: string,
type?: TrendingVideoType type?: TrendingVideoType
@ -32,6 +38,23 @@ export const getTrending = async (
return data; return data;
}; };
const getSearchSuggestions = async (
baseUrl: string,
query: string
): Promise<Suggestions> => {
const url = new URL(apiPath("search", "suggestions"), baseUrl);
const response = await ky.get(url, {
searchParams: { q: query }
});
const json = await response.json();
const data = SuggestionsModel.parse(json);
return data;
};
const adapter: Adapter = { const adapter: Adapter = {
apiType: ApiType.Invidious, apiType: ApiType.Invidious,
@ -39,6 +62,10 @@ const adapter: Adapter = {
return { return {
getTrending(region) { getTrending(region) {
return getTrending(url, region).then(Transformer.trending); return getTrending(url, region).then(Transformer.trending);
},
getSearchSuggestions(query) {
return getSearchSuggestions(url, query).then(Transformer.suggestions);
} }
}; };
} }

@ -1,6 +1,8 @@
import { TrendingVideo } from "@/client/typings/trending"; import { TrendingVideo } from "@/client/typings/trending";
import InvidiousTrending from "./typings/trending"; import InvidiousTrending from "./typings/trending";
import InvidiousSuggestions from "./typings/search/suggestions";
import { Suggestions } from "@/client/typings/search/suggestions";
export default class Transformer { export default class Transformer {
public static trending(data: InvidiousTrending[]): TrendingVideo[] { public static trending(data: InvidiousTrending[]): TrendingVideo[] {
@ -28,4 +30,8 @@ export default class Transformer {
}; };
}); });
} }
public static suggestions(data: InvidiousSuggestions): Suggestions {
return data.suggestions;
}
} }

@ -0,0 +1,10 @@
import z from "zod";
export const SuggestionsModel = z.object({
query: z.string(),
suggestions: z.string().array()
});
type Suggestions = z.infer<typeof SuggestionsModel>;
export default Suggestions;

@ -1,3 +1,4 @@
import z from "zod";
import ky from "ky"; import ky from "ky";
import Adapter, { ApiType } from "@/client/adapters"; import Adapter, { ApiType } from "@/client/adapters";
@ -5,8 +6,9 @@ import Adapter, { ApiType } from "@/client/adapters";
import Trending, { TrendingModel } from "./typings/trending"; import Trending, { TrendingModel } from "./typings/trending";
import Transformer from "./transformer"; import Transformer from "./transformer";
import { Suggestions } from "@/client/typings/search/suggestions";
export const getTrending = async ( const getTrending = async (
apiBaseUrl: string, apiBaseUrl: string,
region = "US" region = "US"
): Promise<Trending[]> => { ): Promise<Trending[]> => {
@ -23,6 +25,23 @@ export const getTrending = async (
return data; return data;
}; };
const getSearchSuggestions = async (
apiBaseUrl: string,
query: string
): Promise<Suggestions> => {
const url = new URL("suggestions", apiBaseUrl);
const response = await ky.get(url, {
searchParams: { query: query }
});
const json = await response.json();
const data = z.string().array().parse(json);
return data;
};
const adapter: Adapter = { const adapter: Adapter = {
apiType: ApiType.Piped, apiType: ApiType.Piped,
@ -30,6 +49,10 @@ const adapter: Adapter = {
return { return {
getTrending(region) { getTrending(region) {
return getTrending(url, region).then(Transformer.trending); return getTrending(url, region).then(Transformer.trending);
},
getSearchSuggestions(query) {
return getSearchSuggestions(url, query);
} }
}; };
} }

@ -3,7 +3,8 @@ import { TrendingVideo } from "./typings/trending";
import InvidiousAdapter from "./adapters/invidious"; import InvidiousAdapter from "./adapters/invidious";
import PipedAdapter from "./adapters/piped"; import PipedAdapter from "./adapters/piped";
import Adapter, { ApiType } from "./adapters"; import Adapter, { ApiType, ConnectedAdapter } from "./adapters";
import { Suggestions } from "./typings/search/suggestions";
export interface RemoteApi { export interface RemoteApi {
type: ApiType; type: ApiType;
@ -24,7 +25,7 @@ export default class Client {
this.apis = apis.map((api) => ({ ...api, score: 0 })); this.apis = apis.map((api) => ({ ...api, score: 0 }));
} }
private getAdapterForApiType(apiType: ApiType): Adapter { private findAdapterForApiType(apiType: ApiType): Adapter {
const adapter = this.adapters.find((adapter) => adapter.apiType == apiType); const adapter = this.adapters.find((adapter) => adapter.apiType == apiType);
if (adapter === undefined) if (adapter === undefined)
@ -39,11 +40,23 @@ export default class Client {
return this.apis[randomIndex]; return this.apis[randomIndex];
} }
public async getTrending(region: string): Promise<TrendingVideo[]> { private getBestAdapter(): ConnectedAdapter {
const api = this.getBestApi(); const api = this.getBestApi();
const adapter = this.getAdapterForApiType(api.type); const adapter = this.findAdapterForApiType(api.type);
return adapter.connect(api.baseUrl);
}
public async getTrending(region: string): Promise<TrendingVideo[]> {
const adapter = this.getBestAdapter();
return await adapter.getTrending(region);
}
public async getSearchSuggestions(query: string): Promise<Suggestions> {
const adapter = this.getBestAdapter();
return await adapter.connect(api.baseUrl).getTrending(region); return await adapter.getSearchSuggestions(query);
} }
} }

@ -0,0 +1 @@
export type Suggestions = string[];

@ -12,6 +12,7 @@ import { Button } from "@nextui-org/button";
import NextLink from "next/link"; import NextLink from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Search } from "./Search";
export const Nav: Component = () => { export const Nav: Component = () => {
const navItems = [ const navItems = [

@ -0,0 +1,52 @@
"use client";
import { useClient } from "@/hooks/useClient";
import { Component } from "@/typings/component";
import { Autocomplete, AutocompleteItem } from "@nextui-org/autocomplete";
import { useQuery } from "@tanstack/react-query";
import { FormEventHandler, useState } from "react";
import { useDebounce } from "use-debounce";
export const Search: Component<{ initialQueryValue?: string }> = ({
initialQueryValue
}) => {
const client = useClient();
const [searchQuery, setSearchQuery] = useState(initialQueryValue ?? "");
const [searchQueryDebounced] = useDebounce(searchQuery, 500);
const { isLoading, error, data } = useQuery({
queryKey: ["search", "suggestions", searchQueryDebounced],
queryFn: () => client.getSearchSuggestions(searchQueryDebounced),
enabled: searchQueryDebounced.length !== 0
});
const handleSubmit: FormEventHandler = (e) => {
// e.preventDefault();
console.log(searchQuery);
};
const suggestions = data ?? [];
return (
<Autocomplete
isClearable
value={searchQuery}
isLoading={isLoading}
defaultInputValue={initialQueryValue}
onSubmit={handleSubmit}
onValueChange={setSearchQuery}
label="Search"
variant="flat"
placeholder="Search for videos"
>
{suggestions.map((suggestion) => (
<AutocompleteItem key={suggestion.toLowerCase()}>
{suggestion}
</AutocompleteItem>
))}
</Autocomplete>
);
};

@ -6446,6 +6446,11 @@ use-composed-ref@^1.3.0:
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
use-debounce@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.0.tgz#5091b18d6c16292605f588bae3c0d2cfae756ff2"
integrity sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==
use-isomorphic-layout-effect@^1.1.1: use-isomorphic-layout-effect@^1.1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"

Loading…
Cancel
Save