feat: filters (to refactor)
This commit is contained in:
parent
dd58b8bdf9
commit
e18c144ba6
8 changed files with 288 additions and 14 deletions
|
@ -38,7 +38,7 @@
|
|||
{#if puzzle.tags?.length}
|
||||
<div class="flex items-center gap-x-6">
|
||||
<div class="flex gap-x-2 text-sm">
|
||||
{#each puzzle.tags as tag}
|
||||
{#each puzzle.tags.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name.toLowerCase())) as tag}
|
||||
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
|
||||
{tag.name}
|
||||
</span>
|
||||
|
@ -63,7 +63,7 @@
|
|||
<div class="flex items-center gap-x-6">
|
||||
{#if puzzle.tags?.length}
|
||||
<div class="flex gap-x-2 text-sm">
|
||||
{#each puzzle.tags as tag}
|
||||
{#each puzzle.tags.filter( (tag) => ['easy', 'medium', 'hard'].includes(tag.name.toLowerCase()) ) as tag}
|
||||
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
|
||||
{tag.name}
|
||||
</span>
|
||||
|
|
34
src/lib/components/ui/select/index.ts
Normal file
34
src/lib/components/ui/select/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
|
||||
const Root = SelectPrimitive.Root;
|
||||
const Group = SelectPrimitive.Group;
|
||||
const Input = SelectPrimitive.Input;
|
||||
const Value = SelectPrimitive.Value;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Input,
|
||||
Label,
|
||||
Item,
|
||||
Value,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Input as SelectInput,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Value as SelectValue,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
};
|
39
src/lib/components/ui/select/select-content.svelte
Normal file
39
src/lib/components/ui/select/select-content.svelte
Normal file
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { scale } from "svelte/transition";
|
||||
import { cn, flyAndScale } from "$lib/utils.js";
|
||||
|
||||
type $$Props = SelectPrimitive.ContentProps;
|
||||
type $$Events = SelectPrimitive.ContentEvents;
|
||||
|
||||
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||
export let inTransition: $$Props["inTransition"] = flyAndScale;
|
||||
export let inTransitionConfig: $$Props["inTransitionConfig"] = undefined;
|
||||
export let outTransition: $$Props["outTransition"] = scale;
|
||||
export let outTransitionConfig: $$Props["outTransitionConfig"] = {
|
||||
start: 0.95,
|
||||
opacity: 0,
|
||||
duration: 50,
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Content
|
||||
{inTransition}
|
||||
{inTransitionConfig}
|
||||
{outTransition}
|
||||
{outTransitionConfig}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
>
|
||||
<div class="w-full p-1">
|
||||
<slot />
|
||||
</div>
|
||||
</SelectPrimitive.Content>
|
40
src/lib/components/ui/select/select-item.svelte
Normal file
40
src/lib/components/ui/select/select-item.svelte
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = SelectPrimitive.ItemProps;
|
||||
type $$Events = SelectPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export let label: $$Props["label"] = undefined;
|
||||
export let disabled: $$Props["disabled"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
{value}
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<slot>
|
||||
{label || value}
|
||||
</slot>
|
||||
</SelectPrimitive.Item>
|
16
src/lib/components/ui/select/select-label.svelte
Normal file
16
src/lib/components/ui/select/select-label.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = SelectPrimitive.LabelProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Label
|
||||
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</SelectPrimitive.Label>
|
11
src/lib/components/ui/select/select-separator.svelte
Normal file
11
src/lib/components/ui/select/select-separator.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = SelectPrimitive.SeparatorProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} />
|
27
src/lib/components/ui/select/select-trigger.svelte
Normal file
27
src/lib/components/ui/select/select-trigger.svelte
Normal file
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = SelectPrimitive.TriggerProps;
|
||||
type $$Events = SelectPrimitive.TriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
let:builder
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot {builder} />
|
||||
<div>
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</SelectPrimitive.Trigger>
|
|
@ -1,26 +1,92 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { PageData, Snapshot } from './$types';
|
||||
|
||||
import BarChart from 'lucide-svelte/icons/bar-chart-2';
|
||||
import Users from 'lucide-svelte/icons/users';
|
||||
|
||||
import Puzzle from '$lib/components/puzzle.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// TODO: Refactor, this is for event purpose only, this is a mess
|
||||
|
||||
type Filters = {
|
||||
name: string;
|
||||
themeList?: string;
|
||||
difficultyList?: string;
|
||||
};
|
||||
|
||||
let name = '';
|
||||
let themeList: string[] = [];
|
||||
let difficultyList: string[] = [];
|
||||
|
||||
const difficulties = data.chapter?.puzzles?.reduce((acc, puzzle) => {
|
||||
const tag = puzzle.tags?.find((tag) =>
|
||||
['easy', 'medium', 'hard'].includes(tag.name.toLowerCase())
|
||||
);
|
||||
|
||||
if (tag) {
|
||||
acc[tag.name] = tag.name.toLowerCase();
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}) as Record<string, string>;
|
||||
|
||||
const themes = data.chapter?.puzzles?.reduce((acc, puzzle) => {
|
||||
puzzle.tags?.forEach((tag) => {
|
||||
if (!['easy', 'medium', 'hard'].includes(tag.name.toLowerCase())) {
|
||||
acc[tag.name] = tag.name.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {}) as Record<string, string>;
|
||||
|
||||
$: filteredPuzzles =
|
||||
data.chapter?.puzzles?.filter((puzzle) => {
|
||||
const regex = new RegExp(name, 'i');
|
||||
|
||||
const nameMatch = regex.test(puzzle.name);
|
||||
|
||||
const themeMatch = themeList.length
|
||||
? puzzle.tags?.some((tag) => themeList.includes(tag.name.toLowerCase()))
|
||||
: true;
|
||||
|
||||
const difficultyMatch = difficultyList.length
|
||||
? puzzle.tags?.some((tag) => difficultyList.includes(tag.name.toLowerCase()))
|
||||
: true;
|
||||
|
||||
return nameMatch && themeMatch && difficultyMatch;
|
||||
}) ?? [];
|
||||
|
||||
export const snapshot: Snapshot<Filters> = {
|
||||
capture: () => ({
|
||||
name,
|
||||
themeList: themeList.length ? themeList.join(',') : '',
|
||||
difficultyList: difficultyList.length ? difficultyList.join(',') : ''
|
||||
}),
|
||||
restore: (value) => {
|
||||
name = value.name;
|
||||
themeList = value.themeList ? value.themeList.split(',') : [];
|
||||
difficultyList = value.difficultyList ? value.difficultyList.split(',') : [];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col gap-2">
|
||||
<header class="flex flex-col justify-between gap-2 lg:flex-row lg:items-center">
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-xl font-semibold">{data.chapter.name}</h2>
|
||||
{#if data.chapter?.puzzles?.length}
|
||||
{#if !data.chapter?.puzzles?.length}
|
||||
<p class="text-muted-foreground">Le chapitre ne contient pour l'instant aucun puzzle</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">
|
||||
Ils vous restent {data.chapter.puzzles.filter((p) => !p.score).length} puzzles à résoudre sur
|
||||
un total de {data.chapter.puzzles.length}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">Le chapitre ne contient pour l'instant aucun puzzle</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
|
@ -36,16 +102,57 @@
|
|||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<Input bind:value={name} placeholder="Rechercher un puzzle" />
|
||||
<Select.Root
|
||||
multiple
|
||||
selected={themeList.map((theme) => ({ label: theme, value: theme }))}
|
||||
onSelectedChange={(v) => {
|
||||
if (v) {
|
||||
themeList = v.map((item) => item.value);
|
||||
} else {
|
||||
themeList = [];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="md:w-[180px]">
|
||||
<Select.Value placeholder="Thème" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.entries(themes) as [key, value]}
|
||||
<Select.Item {value}>{key}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Select.Root
|
||||
multiple
|
||||
selected={difficultyList.map((difficulty) => ({ label: difficulty, value: difficulty }))}
|
||||
onSelectedChange={(v) => {
|
||||
if (v) {
|
||||
difficultyList = v.map((item) => item.value);
|
||||
} else {
|
||||
difficultyList = [];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="md:w-[180px]">
|
||||
<Select.Value placeholder="Difficulté" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.entries(difficulties) as [key, value]}
|
||||
<Select.Item {value}>{key}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
{#if data.chapter?.puzzles?.length}
|
||||
{#each data.chapter.puzzles as puzzle (puzzle.id)}
|
||||
{#if !filteredPuzzles.length}
|
||||
<li class="flex h-16 w-full items-center justify-center rounded border border-border bg-card">
|
||||
<p class="text-muted-foreground">Aucun puzzle trouvé</p>
|
||||
</li>
|
||||
{:else}
|
||||
{#each filteredPuzzles as puzzle (puzzle.id)}
|
||||
<Puzzle {puzzle} />
|
||||
{:else}
|
||||
<li
|
||||
class="flex h-16 w-full items-center justify-center rounded border border-border bg-card"
|
||||
>
|
||||
<p class="text-muted-foreground">Aucun puzzle trouvé</p>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
|
|
Loading…
Add table
Reference in a new issue