Skip to content

Commit

Permalink
feat: added support for filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
cecilia-sanare committed Oct 21, 2024
1 parent ae497d9 commit 3b1f7a6
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/components/layout/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const AppHeader: FC<Props> = ({ onChange }) => {
Protontweaks
</Link>
<div className="flex flex-1 gap-4">
<Input value={search} placeholder="Search" onChange={onChange} />
<Input value={search} placeholder="Search (e.g. 'Frostpunk', 'has:tricks')" onChange={onChange} />
<AppQuickEdit id={id} />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/service/protontweaks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function getApp(id: string) {
return getComputedApp(await fetch<App>(`https://api.protontweaks.com/v4/${id}.json`));
}

export function getComputedApp<T extends ThinApp>(app: T): ComputedApp<T> {
export function getComputedApp<T extends Omit<ThinApp, 'has'>>(app: T): ComputedApp<T> {
return {
...app,
image_url: `https://steamcdn-a.akamaihd.net/steam/apps/${app.id}/header.jpg`,
Expand Down
11 changes: 9 additions & 2 deletions src/types/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ export type AppsList = ApiInfo & {
apps: ThinApp[];
};

export type ThinApp = Pick<App, 'id' | 'name' | 'created_at' | 'updated_at'>;
export type ThinApp = Pick<App, 'id' | 'name' | 'created_at' | 'updated_at'> & {
has: {
args: boolean;
env: boolean;
settings: boolean;
tricks: boolean;
};
};

export type App = {
id: string;
Expand All @@ -29,7 +36,7 @@ export type App = {
updated_at: string;
};

export type ComputedApp<T extends ThinApp = App> = T & {
export type ComputedApp<T extends Omit<ThinApp, 'has'> = App> = T & {
image_url: string;
badges: {
is_new: boolean;
Expand Down
12 changes: 12 additions & 0 deletions src/utils/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function every<K extends string, V, O extends Record<K, V>>(
object: Partial<Record<K, V>>,
predicate: ([key, value]: [K, V | undefined]) => boolean
): boolean {
for (const key in object) {
if (!predicate([key, object[key]])) {
return false;
}
}

return true;
}
49 changes: 46 additions & 3 deletions src/workers/search.worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getApps } from '@/service/protontweaks.service';
import type { App, ThinApp } from '@/types';
import { every } from '@/utils/object';
import { delay } from '@ribbon-studios/js-utils';

self.onmessage = async (event) => {
Expand All @@ -15,21 +17,62 @@ self.onmessage = async (event) => {
}
};

type ParsedSearchOptions = {
raw: string;
query: string;
partials: string[];
has: Partial<Record<keyof ThinApp['has'], boolean>>;
};

function parseOptions(value: string): ParsedSearchOptions {
const has_filters = Array.from(value.matchAll(/has:([^\s]+)/g));

const has = has_filters.reduce<ParsedSearchOptions['has']>((output, [, flag]) => {
if (['trick', 'tricks'].includes(flag)) {
output.tricks = true;
} else if (['env', 'envs'].includes(flag)) {
output.env = true;
} else if (['setting', 'settings'].includes(flag)) {
output.settings = true;
} else if (['arg', 'args'].includes(flag)) {
output.args = true;
}

return output;
}, {});

const query = value
.replace(/has:[^\s]+/g, '')
.replace(/\s\s/g, ' ')
.trim();

return {
raw: value,
query,
partials: query.split(' ').filter(Boolean),
has,
};
}

async function filterApps(value: string) {
// TODO: Add a method of getting an api version (git sha) and using it to enable caching and cache busting
const apps = await getApps();

if (!value) return apps;

const partials = value.split(' ').filter(Boolean);
const options = parseOptions(value);

return apps
.filter((app) => {
return partials.some((partial) => app.name.toLowerCase().includes(partial));
return (
(options.partials.length === 0 ||
options.partials.some((partial) => app.name.toLowerCase().includes(partial))) &&
every(options.has, ([key, value]) => app.has[key] === value)
);
})
.map((app) => ({
...app,
strength: determineMatchStrength(value, partials, app.name.toLowerCase()),
strength: determineMatchStrength(value, options.partials, app.name.toLowerCase()),
}))
.sort((a, b) => b.strength - a.strength);
}
Expand Down

0 comments on commit 3b1f7a6

Please sign in to comment.