Merge pull request 'Filters (to refactor)' (#27) from dev into main

Reviewed-on: #27
This commit is contained in:
glazk0 2024-04-16 02:24:51 +02:00
commit 5ba2101877
8 changed files with 288 additions and 14 deletions

View file

@ -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>

View 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,
};

View 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>

View 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>

View 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>

View 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} />

View 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>

View file

@ -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>