Skip to content

Commit

Permalink
Merge pull request #11 from sergio222-dev/develop
Browse files Browse the repository at this point in the history
* refactor search card
  • Loading branch information
sergio222-dev authored Aug 3, 2024
2 parents f0d8923 + 14db423 commit df5ca8c
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 169 deletions.
10 changes: 10 additions & 0 deletions src/components/chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { HTMLAttributes} from "@builder.io/qwik";
import { component$, Slot } from "@builder.io/qwik";

export const Chip = component$<HTMLAttributes<HTMLDivElement>>(({ class: className, ...props }) => {
return (
<div class={`rounded-3xl bg-secondary px-2 py-1 text-black ${className}`} {...props}>
<Slot />
</div>
);
});
2 changes: 1 addition & 1 deletion src/components/deckCard/DeckCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const DeckCard = component$<DeckCardProps>(({ id, splashArt, name, path =
{...(splashArt ? {
style: {
backgroundImage: `radial-gradient(transparent, rgb(0, 0, 0)), url(${supabase.storage.from(
'CardImages/cards').getPublicUrl(splashArt + '.webp').data.publicUrl})`
'CardImages/cards').getPublicUrl(splashArt).data.publicUrl})`
}
} : {})}
>
Expand Down
60 changes: 60 additions & 0 deletions src/components/filterField/FilterField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { HTMLAttributes, InputHTMLAttributes, JSXOutput, QRL } from "@builder.io/qwik";
import { $, useSignal } from "@builder.io/qwik";
import { Slot } from "@builder.io/qwik";
import { component$ } from "@builder.io/qwik";
import { ButtonIcon } from "~/components/button";
import { Chip } from "~/components/chip/Chip";
import { Icon } from "~/components/icons/Icon";

interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
}

export const ChipFilter = component$<FilterChipProps>(({ ...props }) => {
return (
<Chip class="flex gap-1 hover:bg-red-500 cursor-pointer px-3 py-2 text-white text-sm" {...props}>
<ButtonIcon class="bg-transparent">
<Icon name={'close'} width={8} height={8}/>
</ButtonIcon>
<Slot/>
</Chip>
)
});

interface FilterFieldProps {
onSubmit?: QRL<(this: FilterFieldProps, value: string | undefined) => Promise<void>>;
children?: JSXOutput[];
}

export const FilterField = component$<FilterFieldProps>(
({
onSubmit,
}) => {
const r = useSignal<HTMLFormElement>();

const handleSubmit = $(() => {
// get value
const value = new FormData(r.value).get('contains');

r.value?.reset();

onSubmit?.(value?.toString());
})

return (
<div class="relative w-full rounded-3xl p-2 ring-primary ring-4 focus-within:ring-secondary flex gap-2 flex-wrap">
<Slot/>
<form
ref={r}
class="w-full flex-1 min-w-[200px]"
onSubmit$={handleSubmit}
preventdefault:submit>
<input
name="contains"
autocomplete="off"
class="focus:outline-none flex-1 w-full"
type="text"
/>
</form>
</div>
)
});
13 changes: 10 additions & 3 deletions src/components/icons/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import type { JSXOutput, SVGProps } from "@builder.io/qwik";
import { component$ } from "@builder.io/qwik";

const ICONS: Record<IconName, (props: SVGProps<SVGElement>) => JSXOutput> = {
'art': (props: SVGProps<SVGElement>) => (
'art': (props: SVGProps<SVGElement>) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path
d="M0 64C0 28.7 28.7 0 64 0L352 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64L64 192c-35.3 0-64-28.7-64-64L0 64zM160 352c0-17.7 14.3-32 32-32l0-16c0-44.2 35.8-80 80-80l144 0c17.7 0 32-14.3 32-32l0-32 0-90.5c37.3 13.2 64 48.7 64 90.5l0 32c0 53-43 96-96 96l-144 0c-8.8 0-16 7.2-16 16l0 16c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32-14.3-32-32l0-128z"/>
</svg>)
</svg>),
'close': (props: SVGProps<SVGElement>) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512">
<path
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/>
</svg>
)
}

type IconName = 'art';
type IconName = 'art' | 'close';

interface IconProps extends SVGProps<SVGElement> {
name: IconName;
Expand Down
95 changes: 27 additions & 68 deletions src/features/cards/components/CardFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,42 @@
import { $, component$, useContext, useStore, useTask$ } from '@builder.io/qwik';
import { Menu } from '~/components/menu';
import { MenuTw } from '~/components/menuTw/MenuTw';
import { MenuTwItem } from '~/components/menuTw/MenuTwItem';
import { cardFilter, sortDirection } from '~/config/cardFilter';
import { useSubtypeLoader } from '~/routes/cards';
import type { CardFilters } from '~/stores/filterContext';
import { FilterContext } from '~/stores/filterContext';
import { getDefaultFilter } from '~/utils/cardFilters';
import { useDebounce } from '~/utils/useDebounce';
import { Pagination } from './Pagination';
import { $, component$, useContext } from '@builder.io/qwik';
import { ChipFilter, FilterField } from "~/components/filterField/FilterField";
import type { Filter } from "~/models/filters/Filter";
import { FILTERS_TYPES } from "~/models/filters/Filter";
import { FilterContext } from '~/stores/filterContext';
import { Pagination } from './Pagination';

// TODO: refactor this components, move to common components folder and remove all loaders and context
export const CardFilter = component$(() => {
// Loaders
const subtypes = useSubtypeLoader();

// Context
const c = useContext(FilterContext);

// State
const filters = useStore<CardFilters>(getDefaultFilter(1));

// Side effects
useTask$(({ track }) => {
track(() => filters.sortBy);
track(() => filters.sortDirection);
track(() => filters.name);
track(() => filters.types);
// Handlers
const addContainsFilter = $(async (value: string | undefined) => {
if (!value) return;

void c.updateFilters(filters);
});
const textFilter: Filter = {
id: Math.random().toString(),
label: `Contains: "${value}"`,
value: value,
field: 'text',
filterType: FILTERS_TYPES.LIKE,
isDisjunctive: true,
}

// Handlers
const debounceUpdateName = useDebounce(250, $((event: Event) => {
const value = (event.target as HTMLInputElement).value;
// await c.addFilter(nameFilter);
await c.addFilter(textFilter);
})

void c.updateName(value);
}));

// Render
return (
<div class="flex justify-between">
<form
preventdefault:submit
>
<Menu
onChange$={(e) => filters.sortBy = (e.target as HTMLInputElement).value}
name="sortBy">
{cardFilter.map(s => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</Menu>

<Menu
onChange$={(e) => {
filters.sortDirection = (e.target as HTMLInputElement).value as 'asc' | 'desc';
}}
name="sortDirection">
{sortDirection.map(s => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</Menu>

<MenuTw
onChange={$((s) => {
filters.types = s;
})}
// form={el.value} onChange={$((s) => { subtypesFilter.value = s })}
>
{subtypes.value.map(s => (
<MenuTwItem key={s} label={s} value={s} />
))}
</MenuTw>

<input name="name" onInput$={e => {
void debounceUpdateName(e);
}} type="text" placeholder="Name" />
</form>
<Pagination />
<div class="flex justify-between items-center">
<FilterField onSubmit={addContainsFilter}>
{c.filters.map(f => (
<ChipFilter key={f.id} onClick$={() => c.removeFilter(f.id)}>{f.label}</ChipFilter>
))}
</FilterField>
<Pagination/>
</div>
);
});
2 changes: 1 addition & 1 deletion src/features/cards/components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const Pagination = component$(() => {
</div>
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div class="px-2">
<p class="text-sm text-gray-400">
<p class="text-sm text-gray-400 min-w-max">
Showing {(c.page - 1) * c.size + 1} to {!isLastPage.value ? c.page * c.size : c.count} results
of {c.count}
</p>
Expand Down
5 changes: 3 additions & 2 deletions src/features/cards/server/fetchCards.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RequestEventBase, server$, ServerFunction, z } from '@builder.io/qwik-city';
import { Card } from '~/models/Card';
import { FetchCardsPayload } from "~/models/infrastructure/FetchCardsPayload";
import { cardGetScheme } from "~/models/schemes/cardGet";
import { CardRepository } from "~/providers/repositories/CardRepository";

Expand All @@ -8,9 +9,9 @@ interface ServerCardResponse {
count: number;
}

type ServerCardRequest = (this: RequestEventBase,filters: z.infer<typeof cardGetScheme>) => Promise<ServerCardResponse>
type ServerCardRequest = (this: RequestEventBase,filters: FetchCardsPayload) => Promise<ServerCardResponse>

export const serverCard = server$<ServerCardRequest>(async function (filters: z.infer<typeof cardGetScheme>) {
export const serverFetchCards = server$<ServerCardRequest>(async function (filters ) {
const cardRepo = new CardRepository(this);

const cards = await cardRepo.getCardList(filters);
Expand Down
Empty file added src/models/filters/Contains.ts
Empty file.
24 changes: 24 additions & 0 deletions src/models/filters/Filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const FILTERS_TYPES = {
LIKE: 'LIKE',
IN: 'IN',
}

export interface Filter {
id: string;
label: string;
value: string;
field: string;
filterType: typeof FILTERS_TYPES[keyof typeof FILTERS_TYPES];
isDisjunctive?: boolean;
}

export function convertToFilter(filter: Filter): [string, string, string] {
switch (filter.filterType) {
case FILTERS_TYPES.LIKE:
return [filter.field, 'ilike', '%' + filter.value + '%'];
case FILTERS_TYPES.IN:
return [filter.field, 'in', `(${filter.value})`];
default:
throw new Error('Filter type not found');
}
}
9 changes: 9 additions & 0 deletions src/models/infrastructure/FetchCardsPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Filter } from "~/models/filters/Filter";

export interface FetchCardsPayload {
page: number;
size: number;
sortBy: string;
sortDirection: 'asc' | 'desc';
filters: Filter[];
}
12 changes: 4 additions & 8 deletions src/providers/loaders/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ export const useCardsLoader = routeLoader$(async (requestEnv) => {

const filter = getDefaultFilter();

const filterParse = cardGetScheme.parse(filter)
const cards = await cardRepo.getCardList(filter);

const cards = await cardRepo.getCardList(filterParse);

const count = await cardRepo.getCount(filterParse);
const count = await cardRepo.getCount(filter);

return {
cards,
Expand All @@ -51,11 +49,9 @@ export const useCardDeckLoader = routeLoader$(async (requestEnv) => {

const filter = getDefaultFilter(24);

const filterParse = cardGetScheme.parse(filter)

const cards = await cardRepo.getCardList(filterParse);
const cards = await cardRepo.getCardList(filter);

const count = await cardRepo.getCount(filterParse);
const count = await cardRepo.getCount(filter);

return {
cards,
Expand Down
42 changes: 22 additions & 20 deletions src/providers/repositories/CardRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@ import type { RequestEventBase, RequestEventLoader } from '@builder.io/qwik-city
import { Logger } from '~/lib/logger';
import { createClientServer } from '~/lib/supabase-qwik';
import type { Card } from '~/models/Card';
import { convertToFilter } from "~/models/filters/Filter";
import type { FetchCardsPayload } from "~/models/infrastructure/FetchCardsPayload";
import { getCardImageUrl } from '~/utils/cardImage';

interface CardListFilter {
sortBy: string;
sortDirection: 'asc' | 'desc';
types: string[];
page: number;
size: number;
name: string;
}

export class CardRepository {

private readonly request: RequestEventLoader | RequestEventBase;
Expand All @@ -21,19 +14,19 @@ export class CardRepository {
this.request = request;
}

public async getCount(filter: CardListFilter): Promise<number> {
public async getCount(filter: FetchCardsPayload): Promise<number> {
const supabase = createClientServer(this.request);

let query = supabase
.from('cards')
.select('*', { count: 'exact', head: true })
.or(`name.ilike.%${filter.name}%`)
.order(filter.sortBy, { ascending: filter.sortDirection === 'asc' })
.range((Number(filter.page) - 1) * Number(filter.size), (Number(filter.page) * Number(filter.size)) - 1);

if (filter.types.length > 0) {
query = query.in('subtype', filter.types);
}
filter.filters.forEach(f => {
const [field, operator, value] = convertToFilter(f);
query = query.filter(field, operator, value);
})

const { count, error } = await query;

Expand Down Expand Up @@ -63,19 +56,28 @@ export class CardRepository {
return data[0];
}

public async getCardList(filter: CardListFilter): Promise<Card[]> {
public async getCardList(filter: FetchCardsPayload): Promise<Card[]> {
const supabase = createClientServer(this.request);


let query = supabase
.from('cards')
.select()
.or(`name.ilike.%${filter.name}%`)
.order(filter.sortBy, { ascending: filter.sortDirection === 'asc' })
.range((Number(filter.page) - 1) * Number(filter.size), (Number(filter.page) * Number(filter.size)) - 1);

if (filter.types.length > 0) {
query = query.in('subtype', filter.types);
}
const disjunctiveFilters = filter.filters.filter(f => f.isDisjunctive);

disjunctiveFilters.forEach(f => {
const [field, operator, value] = convertToFilter(f);
query = query.or(`${field}.${operator}.${value}`);
})

filter.filters.filter(f => !f.isDisjunctive).forEach(f => {
const [field, operator, value] = convertToFilter(f);
query = query.filter(field, operator, value);
})


const { data, error } = await query;

Expand All @@ -90,7 +92,7 @@ export class CardRepository {
return data.map(c => {
return {
...c,
image: getCardImageUrl(c.image + '.webp', this.request)
image: getCardImageUrl(c.image, this.request)
};
});
}
Expand Down
Loading

0 comments on commit df5ca8c

Please sign in to comment.