diff --git a/.dockerignore b/.dockerignore index ae710bb8..5a5e051b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ ** !docker/ !build/ -!package.json \ No newline at end of file +!package.json +!pnpm-lock.yaml \ No newline at end of file diff --git a/.env.example b/.env.example index ad6cbf72..30a6d3b4 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,5 @@ -API_URL=http://localhost:3030 # server reachable +API_URI=http://localhost:3030 # server reachable +PUBLIC_API_URI=http://localhost:3030 # client reachable +# use local frontend with official backend (CORS is enabled for http://localhost:5173) +# API_URI=https://api.hearch.co +# PUBLIC_API_URI=https://api.hearch.co diff --git a/src/app.css b/src/app.css index cd506e03..bd6213e1 100644 --- a/src/app.css +++ b/src/app.css @@ -1,23 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; - -.hearchco-bg-primary, -.hover\:hearchco-bg-primary:hover { - background-color: #ffb280; -} - -.hearchco-bg-secondary, -.hover\:hearchco-bg-secondary:hover { - background-color: #2b1100; -} - -.hearchco-text-primary, -.hover\:hearchco-text-primary:hover { - color: #ffb280; -} - -.hearchco-text-secondary, -.hover\:hearchco-text-secondary:hover { - color: #2b1100; -} +@tailwind utilities; \ No newline at end of file diff --git a/src/app.html b/src/app.html index a3f0efcb..2880668a 100644 --- a/src/app.html +++ b/src/app.html @@ -1,4 +1,4 @@ - + diff --git a/src/lib/assets/logo.svg b/src/lib/assets/logo.svg deleted file mode 100644 index b437f102..00000000 --- a/src/lib/assets/logo.svg +++ /dev/null @@ -1,98 +0,0 @@ - - - - diff --git a/src/lib/components/DarkModeToggle.svelte b/src/lib/components/DarkModeToggle.svelte new file mode 100644 index 00000000..2fb429b7 --- /dev/null +++ b/src/lib/components/DarkModeToggle.svelte @@ -0,0 +1,77 @@ + + + diff --git a/src/lib/components/Error.svelte b/src/lib/components/Error.svelte index 050968be..ffaa8517 100644 --- a/src/lib/components/Error.svelte +++ b/src/lib/components/Error.svelte @@ -1,11 +1,11 @@ @@ -20,13 +20,17 @@

- {message} + {err !== undefined ? err.message : message}

- logo -

Go back to homepage

- logo + +

+ Go back to homepage +

+
diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index 7383da6a..354049c2 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -2,8 +2,11 @@ class="mt-auto flex h-20 w-full place-content-center items-center border-t-2 border-gray-100 dark:border-zinc-700" >
- Source code + Source code +
diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 4ba838d1..77f213af 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -1,25 +1,17 @@
-
+
-
- - logo - -
- -
+
diff --git a/src/lib/components/Load.svelte b/src/lib/components/Load.svelte deleted file mode 100644 index 4f73a543..00000000 --- a/src/lib/components/Load.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
-
- {#each { length: 10 } as _} -
-
-
-
-
-
-
-
-
- {/each} -
-
\ No newline at end of file diff --git a/src/lib/components/Logo.svelte b/src/lib/components/Logo.svelte new file mode 100644 index 00000000..b66a0dee --- /dev/null +++ b/src/lib/components/Logo.svelte @@ -0,0 +1,70 @@ + + + + + + + + + + + + + diff --git a/src/lib/components/ResultType.ts b/src/lib/components/ResultType.ts deleted file mode 100644 index f0ede4c0..00000000 --- a/src/lib/components/ResultType.ts +++ /dev/null @@ -1,16 +0,0 @@ -type EngineRank = { - SearchEngine: string; - Rank: number; - Page: number; - OnPageRank: number; -}; - -export type Result = { - URL: string; - Rank: number; - Score: number; - Title: string; - Description: string; - EngineRanks: EngineRank[]; - TimesReturned: number; -}; diff --git a/src/lib/components/Searchbox.svelte b/src/lib/components/Searchbox.svelte index 69d40496..960cc0aa 100644 --- a/src/lib/components/Searchbox.svelte +++ b/src/lib/components/Searchbox.svelte @@ -1,66 +1,120 @@
- - - - + {#if query !== ''} + + {/if} + + + {#if categories} +
+ {/if} +
+ {#if categories} +
+ {#each Object.values(CategoryEnum) as category} + + {/each} +
+ {/if}
diff --git a/src/lib/components/search/display/Display.svelte b/src/lib/components/search/display/Display.svelte new file mode 100644 index 00000000..4600b718 --- /dev/null +++ b/src/lib/components/search/display/Display.svelte @@ -0,0 +1,31 @@ + + + +{#if category === CategoryEnum.IMAGES} + +{:else if category !== undefined} + +{:else} + +{/if} diff --git a/src/lib/components/search/display/general/Pages.svelte b/src/lib/components/search/display/general/Pages.svelte new file mode 100644 index 00000000..c9adce1e --- /dev/null +++ b/src/lib/components/search/display/general/Pages.svelte @@ -0,0 +1,50 @@ + + +
+ + {#each { length: numberOfPages } as _, i} +
+ + +
+ {/each} +
diff --git a/src/lib/components/Result.svelte b/src/lib/components/search/display/general/Result.svelte similarity index 69% rename from src/lib/components/Result.svelte rename to src/lib/components/search/display/general/Result.svelte index 543a75c6..46f1cd0a 100644 --- a/src/lib/components/Result.svelte +++ b/src/lib/components/search/display/general/Result.svelte @@ -1,18 +1,24 @@
{result.URL} -

+

{result.Title}

{result.Description}

diff --git a/src/lib/components/search/display/general/Results.svelte b/src/lib/components/search/display/general/Results.svelte new file mode 100644 index 00000000..8cd20843 --- /dev/null +++ b/src/lib/components/search/display/general/Results.svelte @@ -0,0 +1,25 @@ + + +
+
+ {#each results as result, i (result.URL)} + + {#if i !== results.length - 1} +
+ {/if} + {/each} +
+
+ diff --git a/src/lib/components/search/display/images/Image.svelte b/src/lib/components/search/display/images/Image.svelte new file mode 100644 index 00000000..38bf8458 --- /dev/null +++ b/src/lib/components/search/display/images/Image.svelte @@ -0,0 +1,41 @@ + + +
+ +
diff --git a/src/lib/components/search/display/images/Images.svelte b/src/lib/components/search/display/images/Images.svelte new file mode 100644 index 00000000..cda27049 --- /dev/null +++ b/src/lib/components/search/display/images/Images.svelte @@ -0,0 +1,51 @@ + + +
+ + {#if imgResultPreview !== undefined} +
+ +
+ {/if} +
+
+ {#each results as result (result.URL)} +
+ 1} + class:sm:col-span-2={result.ImageResult.Thumbnail.Height / + (result.ImageResult.Thumbnail.Width * 0.64) < + 1} + class="flex-none" + > + +
+ {/each} +
+
+ + {#if imgResultPreview !== undefined} + + {/if} +
+ diff --git a/src/lib/components/search/display/images/Preview.svelte b/src/lib/components/search/display/images/Preview.svelte new file mode 100644 index 00000000..1560a5d2 --- /dev/null +++ b/src/lib/components/search/display/images/Preview.svelte @@ -0,0 +1,40 @@ + + +
+ +
+ {result.Title} +
+
+ +

+ {result.Title} +

+
+

+ {result.Description} +

+
+ {#each result.EngineRanks as engineRank (engineRank.SearchEngine)} + {engineRank.SearchEngine} + {/each} +
+
diff --git a/src/lib/components/search/display/images/ShowMore.svelte b/src/lib/components/search/display/images/ShowMore.svelte new file mode 100644 index 00000000..36c71f28 --- /dev/null +++ b/src/lib/components/search/display/images/ShowMore.svelte @@ -0,0 +1,35 @@ + + + +
+ +
diff --git a/src/lib/components/search/load/Load.svelte b/src/lib/components/search/load/Load.svelte new file mode 100644 index 00000000..9bf4a2d1 --- /dev/null +++ b/src/lib/components/search/load/Load.svelte @@ -0,0 +1,41 @@ + + +{#if category === CategoryEnum.IMAGES} + +{:else if category !== undefined} + +{:else} + +{/if} + + +{#if false} + + + + + + + + + + + +{/if} diff --git a/src/lib/components/search/load/general/LoadResult.svelte b/src/lib/components/search/load/general/LoadResult.svelte new file mode 100644 index 00000000..ecc8a9f2 --- /dev/null +++ b/src/lib/components/search/load/general/LoadResult.svelte @@ -0,0 +1,14 @@ +
+
+
+
+
+
+
+
diff --git a/src/lib/components/search/load/general/LoadResults.svelte b/src/lib/components/search/load/general/LoadResults.svelte new file mode 100644 index 00000000..5657779d --- /dev/null +++ b/src/lib/components/search/load/general/LoadResults.svelte @@ -0,0 +1,18 @@ + + +
+
+ {#each { length: numberOfResults } as _, i} + + {#if i !== numberOfResults - 1} +
+ {/if} + {/each} +
+
diff --git a/src/lib/components/search/load/images/LoadImage.svelte b/src/lib/components/search/load/images/LoadImage.svelte new file mode 100644 index 00000000..99b4d74b --- /dev/null +++ b/src/lib/components/search/load/images/LoadImage.svelte @@ -0,0 +1,16 @@ + + +
+
+
+
+
diff --git a/src/lib/components/search/load/images/LoadImages.svelte b/src/lib/components/search/load/images/LoadImages.svelte new file mode 100644 index 00000000..f5fd1a77 --- /dev/null +++ b/src/lib/components/search/load/images/LoadImages.svelte @@ -0,0 +1,35 @@ + + +
+
+ {#each fakeImages as result} +
1} + class:sm:col-span-2={result.Height / (result.Width * 0.64) < 1} + class="flex-none" + > + +
+ {/each} +
+
diff --git a/src/lib/functions/categoryFrom.ts b/src/lib/functions/categoryFrom.ts new file mode 100644 index 00000000..5a383e34 --- /dev/null +++ b/src/lib/functions/categoryFrom.ts @@ -0,0 +1,21 @@ +import { CategoryEnum } from '$lib/types/category'; + +export function categoryFrom(query: string): string { + let cat: string = 'general'; + + Object.values(CategoryEnum).some((category) => { + if (query.startsWith('!' + category)) { + cat = category; + } + }); + + return cat; +} + +export function hasCategory(query: string): boolean { + return Object.values(CategoryEnum).some((category) => query.startsWith('!' + category)); +} + +export function categoryEquals(query: string, category: string): boolean { + return category === categoryFrom(query); +} diff --git a/src/lib/functions/createApiUrl.ts b/src/lib/functions/createApiUrl.ts new file mode 100644 index 00000000..c4f2aff7 --- /dev/null +++ b/src/lib/functions/createApiUrl.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; + +export function createApiUrl(path: string, params?: URLSearchParams): string { + const apiUri: string | undefined = env.API_URI; + if (apiUri === undefined) { + throw new Error('API_URI env is not defined'); + } + + const paramsString: string | undefined = params ? params.toString() : undefined; + return (apiUri.endsWith('/') ? apiUri : apiUri + '/') + path + (paramsString ? `?${paramsString}` : ''); +} diff --git a/src/lib/functions/createPublicApiUrl.ts b/src/lib/functions/createPublicApiUrl.ts new file mode 100644 index 00000000..8800a2cb --- /dev/null +++ b/src/lib/functions/createPublicApiUrl.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/public'; + +export function createPublicApiUrl(path: string, params?: URLSearchParams): string { + const apiUri: string | undefined = env.PUBLIC_API_URI; + if (apiUri === undefined) { + throw new Error('PUBLIC_API_URI env is not defined'); + } + + const paramsString: string | undefined = params ? params.toString() : undefined; + return (apiUri.endsWith('/') ? apiUri : apiUri + '/') + path + (paramsString ? `?${paramsString}` : ''); +} diff --git a/src/lib/functions/fetchPublicResultsJSON.ts b/src/lib/functions/fetchPublicResultsJSON.ts new file mode 100644 index 00000000..bd4ea14f --- /dev/null +++ b/src/lib/functions/fetchPublicResultsJSON.ts @@ -0,0 +1,40 @@ +import { error } from '@sveltejs/kit'; +import { createPublicApiUrl } from '$lib/functions/createPublicApiUrl'; + +import type { ResultType, ErrorResponseType } from '$lib/types/result'; + +export async function fetchPublicResultsJSON( + params: URLSearchParams +): Promise { + let apiUrl: string; + try { + apiUrl = createPublicApiUrl('search', params); + } catch (err: any) { + // Internal Server Error + throw error(500, `Failed to create API URL: ${err.message}`); + } + + let response: Response; + try { + response = await fetch(apiUrl); + } catch (err: any) { + // Service Unavailable + throw error(503, `Failed to fetch results: ${err.message}`); + } + + let jsonResponse: ResultType[] | ErrorResponseType; + try { + jsonResponse = await response.json(); + } catch (err: any) { + // Internal Server Error + throw error(500, `Failed to parse results: ${err.message}`); + } + + if ('message' in jsonResponse && 'value' in jsonResponse) { + // same as backend + throw error(response.status, `${jsonResponse.message}: ${jsonResponse.value}`); + } + + const results: ResultType[] = jsonResponse; + return results; +} diff --git a/src/lib/functions/fetchResultsJSON.ts b/src/lib/functions/fetchResultsJSON.ts new file mode 100644 index 00000000..9c51229d --- /dev/null +++ b/src/lib/functions/fetchResultsJSON.ts @@ -0,0 +1,63 @@ +import { error } from '@sveltejs/kit'; +import { sleep } from '$lib/functions/sleep'; +import { createApiUrl } from '$lib/functions/createApiUrl'; + +import type { ResultType, ErrorResponseType } from '$lib/types/result'; + +export async function fetchResultsJSON( + fetch: (input: URL | RequestInfo, init?: RequestInit | undefined) => Promise, + setHeaders: (headers: Record) => void, + params: URLSearchParams, + delay: number +): Promise { + const delayed: Promise = sleep(delay); + + let apiUrl: string; + try { + apiUrl = createApiUrl('search', params); + } catch (err: any) { + await delayed; + // Internal Server Error + throw error(500, `Failed to create API URL: ${err.message}`); + } + + let response: Response; + try { + response = await fetch(apiUrl); + } catch (err: any) { + await delayed; + // Service Unavailable + throw error(503, `Failed to fetch results: ${err.message}`); + } + + let jsonResponse: ResultType[] | ErrorResponseType; + try { + jsonResponse = await response.json(); + } catch (err: any) { + await delayed; + // Internal Server Error + throw error(500, `Failed to parse results: ${err.message}`); + } + + if ('message' in jsonResponse && 'value' in jsonResponse) { + await delayed; + // same as backend + throw error(response.status, `${jsonResponse.message}: ${jsonResponse.value}`); + } + + const age: string | null = response.headers.get('age'); + const cacheControl: string | null = response.headers.get('cache-control'); + setHeaders({ + age: age !== null ? age : '0', + 'cache-control': cacheControl !== null ? cacheControl : 'no-cache' + }); + + const results: ResultType[] = jsonResponse; + await delayed; + return results; +} + +export async function delayFakeFetch(delay: number): Promise { + await sleep(delay); + return []; +} diff --git a/src/lib/functions/fetchResultsLazily.ts b/src/lib/functions/fetchResultsLazily.ts new file mode 100644 index 00000000..066473ff --- /dev/null +++ b/src/lib/functions/fetchResultsLazily.ts @@ -0,0 +1,28 @@ +import { fetchPublicResultsJSON } from "$lib/functions/fetchPublicResultsJSON"; +import type { ResultType } from "$lib/types/result"; + +export async function fetchResultsLazily(query: string, start: number, offset: number, pages: number, results: ResultType[], paramsString: string): Promise<[number, ResultType[]]> { + // create URLParams object from string and set start parameter + const params: URLSearchParams = new URLSearchParams(paramsString); + params.set("q", query); + params.set("start", (start + offset).toString()); + params.set("pages", pages.toString()); + + // get additional results + let additionalResults: ResultType[]; + try { + additionalResults = await fetchPublicResultsJSON(params); + } catch(err: any) { + throw new Error(`Error fetching additional results: ${err.message}`); + } + + // append additional results to combined results while removing duplicates + const combinedResults: ResultType[] = [...results, ...additionalResults.filter((r) => !results.some((result) => result.URL === r.URL))]; + + // adjust new results' ranks to continue from the last rank of the existing results + for (let i: number = results.length; i < combinedResults.length; i++) { + combinedResults[i].Rank = i + 1; + } + + return [offset+pages, combinedResults]; +} \ No newline at end of file diff --git a/src/lib/functions/parseIntParam.ts b/src/lib/functions/parseIntParam.ts new file mode 100644 index 00000000..59c58b18 --- /dev/null +++ b/src/lib/functions/parseIntParam.ts @@ -0,0 +1,13 @@ +export function parseIntParam(url: URL, param: string, fallback: number): number { + const value: string | null = url.searchParams.get(param); + if (value === null) { + return fallback; + } + + const parsed: number = parseInt(value, 10); + if (isNaN(parsed)) { + return fallback; + } + + return parsed; +} diff --git a/src/lib/functions/proxyImageLink.ts b/src/lib/functions/proxyImageLink.ts new file mode 100644 index 00000000..97a6a309 --- /dev/null +++ b/src/lib/functions/proxyImageLink.ts @@ -0,0 +1,18 @@ +import { error } from '@sveltejs/kit'; +import { createPublicApiUrl } from '$lib/functions/createPublicApiUrl'; + +export function proxyImageLink(url: string, hash: string): string { + const params: URLSearchParams = new URLSearchParams(); + params.set('url', url); + params.set('hash', hash); + + let apiUrl: string; + try { + apiUrl = createPublicApiUrl('proxy', params); + } catch (err: any) { + // Internal Server Error + throw error(500, `Failed to create API URL: ${err.message}`); + } + + return apiUrl; +} diff --git a/src/lib/functions/sleep.ts b/src/lib/functions/sleep.ts new file mode 100644 index 00000000..0e8767c6 --- /dev/null +++ b/src/lib/functions/sleep.ts @@ -0,0 +1,3 @@ +export async function sleep(ms: number): Promise { + await new Promise((r) => setTimeout(r, ms)); +} diff --git a/src/lib/types/category.ts b/src/lib/types/category.ts new file mode 100644 index 00000000..26866cb6 --- /dev/null +++ b/src/lib/types/category.ts @@ -0,0 +1,10 @@ +export enum CategoryEnum { + GENERAL = 'general', + IMAGES = 'images' + // INFO = 'info', + // SCIENCE = 'science', + // NEWS = 'news', + // BLOG = 'blog', + // SURF = 'surf', + // NEWNEWS = 'newnews' +} diff --git a/src/lib/types/result.ts b/src/lib/types/result.ts new file mode 100644 index 00000000..ad4e1e37 --- /dev/null +++ b/src/lib/types/result.ts @@ -0,0 +1,36 @@ +type EngineRank = { + SearchEngine: string; + Rank: number; + Page: number; + OnPageRank: number; +}; + +export type ImageFormat = { + Height: number; + Width: number; +}; + +type ImageResult = { + Original: ImageFormat; + Thumbnail: ImageFormat; + ThumbnailURL: string; + ThumbnailURLHash: string; + Source: string; + SourceURL: string; +}; + +export type ResultType = { + URL: string; + URLHash: string; + Rank: number; + Score: number; + Title: string; + Description: string; + EngineRanks: EngineRank[]; + ImageResult: ImageResult; +}; + +export type ErrorResponseType = { + message: string; + value: string; +}; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index 5571804d..ca27576f 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 66d3a1cb..8285409d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,25 @@ - + + + + +