Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sorting: its not working lol #45

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b380cf3
its not working lol
ashlsun Jul 11, 2024
565ea01
refactor: sort type string to string literal
Rettend Jul 11, 2024
0fce6dd
refactor!: global item store
Rettend Jul 11, 2024
c5726d8
refactor: group storage related ops
Rettend Jul 11, 2024
fec5b35
chore: cleanup
Rettend Jul 11, 2024
72dead3
move parsing logic to handler in component, remove importItem
ashlsun Jul 12, 2024
15aff94
deps: bump svelte and fix sorting bruhhhh
Rettend Jul 12, 2024
a3b8350
chore: resolve conflicts
Rettend Jul 12, 2024
415a86e
fix: messed up format in tips
Rettend Jul 12, 2024
c0f6ba3
revert: key with id
Rettend Jul 12, 2024
1ce18b6
feat: add none sort, remove hardcodes
Rettend Jul 12, 2024
c5632db
feat: store sorts in localstorage
Rettend Jul 12, 2024
8c42358
style: change delete text to x icon
Rettend Jul 12, 2024
5622ad0
fix: item count, tests broken by delete icon
Rettend Jul 12, 2024
e43ed5f
fix: preserve selected item when sorting
Rettend Jul 12, 2024
e109cbd
feat: add and rename storages
Rettend Jul 12, 2024
9ca5c04
make x icon a thinner stroke
ashlsun Jul 13, 2024
006edcd
use option/alt + number to select the first item of corresponding sto…
ashlsun Jul 17, 2024
df110db
don't select when storage is an empty list
ashlsun Jul 17, 2024
b53c7cb
fix: remove random idb query
Rettend Jul 17, 2024
133a0f8
feat: add flex wrapping to storages
Rettend Jul 17, 2024
8e6ac1b
fix: prevent scrolling with arrows
Rettend Jul 17, 2024
c02e645
refactor: change sort dropdown handling for tests
Rettend Jul 17, 2024
07b8d8e
test: add basic tests for storageplace
Rettend Jul 17, 2024
f5d663b
fix: ♾️ recursion
Rettend Jul 18, 2024
059d52d
fix ai workflow
ashlsun Jul 21, 2024
539923d
clear selection after input
ashlsun Jul 21, 2024
42cc922
fix: select previous item if deleting last item
ashlsun Jul 21, 2024
9d6c392
handle keyboard navigation at page level instead of item level
ashlsun Jul 26, 2024
a1db555
fix tests & remove obsolete ones
ashlsun Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"globals": "^15.0.0",
"jsdom": "^24.1.0",
"postcss": "^8.4.38",
"svelte": "^5.0.0-next.166",
"svelte": "^5.0.0-next.182",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.4",
"tslib": "^2.4.1",
Expand Down
157 changes: 113 additions & 44 deletions src/lib/components/StoragePlace.svelte
Original file line number Diff line number Diff line change
@@ -1,78 +1,147 @@
<script lang="ts">
import { untrack } from 'svelte'
import Item from '$lib/components/item/Item.svelte'
import { type StoredItem } from '$lib/db'
import { type ItemStore } from '$lib/stores/item.svelte'
import type { SortBy, StoredItem } from '$lib/types'
import { sortBy } from '$lib/types'
import { itemStore } from '$lib/stores/item.svelte'
import { localDb } from '$lib/db'
import { stopPropagation } from '$lib/utils'

// Props
type Props = {
storagePlaceName: string
items: ItemStore | null
storageName: string
}

const {
storagePlaceName,
items,
storageName,
}: Props = $props()

// State
let newItemName = $state('')
let newItemInput = $state('')
let sortOption = $state<SortBy>(localDb.storage.getSort(storageName))
let previousStorageName = storageName
const storageOps = $derived(itemStore.storage(storageName))

// Reactive declarations
$effect(() => {
if (localDb.storage.getSort(storageName) !== sortOption)
localDb.storage.setSort(storageName, sortOption)

untrack(() => storageOps.sortItems(sortOption))
})

// Methods
function addItem() {
if (items)
items.add(newItemName)
newItemName = ''
function inputItem() {
if (newItemInput === '')
return

let name = newItemInput
let quantity = 1

const itemList = newItemInput.split(' ')
if (itemList.length > 1 && itemList[0].match(/^\d+$/)) {
name = newItemInput.slice(itemList[0].length).trim()
quantity = Number(itemList[0])
}
storageOps.addItem({ name, quantity })
sortOption = 'none'
newItemInput = ''
itemStore.clearSelected()
}

// Handlers
function handleInputKeypress(event: KeyboardEvent) {
function handleInputKeydown(event: KeyboardEvent) {
if (event.key === 'Enter')
addItem()
inputItem()
}

function handleSort(event: Event) {
const value = (event.target as HTMLSelectElement).value as SortBy
console.log('sortOption', value)
sortOption = value
}

function handleEnter(event: KeyboardEvent) {
if (event.key !== 'Enter')
return

const target = event.target as HTMLElement
event.preventDefault()
itemStore.updateStorage(previousStorageName, target.textContent || '')
localDb.storage.rename(previousStorageName, target.textContent || '')
previousStorageName = target.textContent || ''
target.blur()
}
</script>

<div
class="rounded-sm border m-3 inline-block h-fit min-w-80 max-w-[420px] border-black p-1"
class="rounded-sm border m-3 inline-block h-fit min-w-80 max-w-[420px] border-black px-2 pb-2"
role="tree"
>
<h1 class="font-bold">{storagePlaceName}
{#if items} <span class="text-stone-400">({items.list.length})</span> {/if}
</h1>
{#if items}
{@const store = items}
<div role="group">
{#each items.list as item, i (item.id)}
<Item
bind:item={items.list[i]}
isSelected={items.selected === i}
onSelected={(amount = 0) => {
store.select(i + amount)
}}
onDelete={() => {
store.delete(item.id)
}}
onUpdate={(updatedItem: StoredItem) => {
store.update(item.id, updatedItem)
}}
/>
{/each}
<div class="flex w-full justify-between items-center ">
<h1>
<span
contenteditable
role="textbox"
aria-label="storage name"
aria-multiline="false"
tabindex="0"
onkeydown={handleEnter}
>
<b>{storageName}</b>
</span>
<span class="text-stone-400">({itemStore.itemCounts[storageName]})</span>
</h1>

<div class="flex items-center">
<span class="text-sm italic text-stone-500 mr-1">sort:</span>
<select
class="text-sm py-1 my-1 italic text-stone-500"
role="listbox"
bind:value={sortOption}
onchange={handleSort}
>
{#each sortBy as sort}
<option value={sort} hidden={sort === 'none'}>{sort}</option>
{/each}
</select>
</div>
{#if items.list.length === 0}
<div class="text-stone-400">Nothing in the {storagePlaceName}.</div>
{/if}
{:else}
<p class="text-stone-400">Loading...</p>

</div>
<div role="group">
{#each itemStore.items[storageName] as item, i (item.id)}
<Item
bind:item={itemStore.items[storageName][i]}
isSelected={itemStore.selected.storage === storageName && itemStore.selected.index === i}
isExpanded={itemStore.expanded && itemStore.selected.storage === storageName && itemStore.selected.index === i}
onSelected={(amount = 0) => {
itemStore.selectItem(storageName, i + amount)
}}
onDelete={() => {
storageOps.deleteItemById(item.id)
if (i === itemStore.items[storageName].length)
itemStore.selectItem(storageName, i - 1)
}}
onToggleExpanded={itemStore.toggleExpanded}
onUpdate={(updatedItem: StoredItem) => {
storageOps.updateItem(updatedItem)
}}
/>
{/each}
</div>
{#if itemStore.itemCounts[storageName] === 0}
<div class="text-stone-400">Nothing in the {storageName}.</div>
{/if}
<input
class="rounded-sm border border-black px-1 transition mt-5 outline-emerald-600 placeholder:text-stone-400 placeholder:italic placeholder:text-sm"
bind:value={newItemName}
onkeypress={handleInputKeypress}
bind:value={newItemInput}
onkeydown={stopPropagation(handleInputKeydown)}
maxlength="20"
placeholder="Add a new item..."
/>
<button
class="transition hover:font-bold hover:text-emerald-600"
onclick={addItem}
class="transition hover:font-bold hover:text-emerald-700"
onclick={inputItem}
>
+
</button>
Expand Down
141 changes: 141 additions & 0 deletions src/lib/components/StoragePlace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { type RenderResult, cleanup, fireEvent, render, within } from '@testing-library/svelte'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import StoragePlace from './StoragePlace.svelte'
import { itemStore } from '$lib/stores/item.svelte'
import { localDb } from '$lib/db'

vi.mock('$lib/db', () => ({
localDb: {
storage: {
getSort: vi.fn().mockReturnValue('none'),
setSort: vi.fn(),
rename: vi.fn(),
},
},
}))

const mockStorageOperations = {
addItem: vi.fn(),
deleteItemById: vi.fn(),
deleteItemByIndex: vi.fn(),
updateItem: vi.fn(),
sortItems: vi.fn(),
getItemById: vi.fn(),
getItemByIndex: vi.fn(),
getItemByName: vi.fn(),
}

vi.mock('$lib/stores/item.svelte', () => {
return {
itemStore: {
items: {},
itemCounts: {},
selected: {},
selectItem: vi.fn(),
clearSelected: vi.fn(),
updateStorage: vi.fn(),
storage: vi.fn(() => mockStorageOperations),
},
}
})

describe('storagePlace Component', () => {
const mockStorageName = 'TestStorage'
let component: RenderResult<StoragePlace>

beforeEach(() => {
vi.resetAllMocks()

itemStore.items[mockStorageName] = []
itemStore.itemCounts[mockStorageName] = 0

vi.mocked(itemStore.storage).mockReturnValue({
...mockStorageOperations,
})

component = render(StoragePlace, { props: { storageName: mockStorageName } })
})

afterEach(() => {
cleanup()
})

it('renders the storage correctly', () => {
const { getByRole, getByText, getByPlaceholderText } = component

expect(getByText(mockStorageName)).toBeTruthy()

const sortDropdown = getByRole('listbox')
expect(sortDropdown).toBeTruthy()

const input = getByPlaceholderText('Add a new item...')
expect(input).toBeTruthy()
})

it('displays the correct item count and message when empty', () => {
const { getByText } = component
expect(getByText('(0)')).toBeTruthy()
expect(getByText(`Nothing in the ${mockStorageName}.`)).toBeTruthy()
})

it('displays the correct item count when items exist', () => {
itemStore.itemCounts[mockStorageName] = 42
component = render(StoragePlace, { props: { storageName: mockStorageName } })
const { getByText } = component
expect(getByText('(42)')).toBeTruthy()
})

it('adds a new item when pressing Enter in the input field', async () => {
const { getByPlaceholderText } = component
const input = getByPlaceholderText('Add a new item...')
await fireEvent.input(input, { target: { value: 'New Item' } })
await fireEvent.keyDown(input, { key: 'Enter', code: 13, charCode: 13 })
expect(itemStore.storage(mockStorageName).addItem).toHaveBeenCalledWith({ name: 'New Item', quantity: 1 })
})

it('adds a new item with quantity when input format is "2 New Item"', async () => {
const { getByPlaceholderText } = component
const input = getByPlaceholderText('Add a new item...')
await fireEvent.input(input, { target: { value: '2 New Item' } })
await fireEvent.keyDown(input, { key: 'Enter', code: 13, charCode: 13 })
expect(itemStore.storage(mockStorageName).addItem).toHaveBeenCalledWith({ name: 'New Item', quantity: 2 })
})

it('clears the input field after adding an item', async () => {
const { getByPlaceholderText } = component
const input = getByPlaceholderText('Add a new item...') as HTMLInputElement
await fireEvent.input(input, { target: { value: 'New Item' } })
await fireEvent.keyDown(input, { key: 'Enter', code: 13, charCode: 13 })
expect(input.value).toBe('')
})

it('changes the sort option when selecting from the dropdown', async () => {
const { getByRole } = component
const sortDropdown = getByRole('listbox')
await fireEvent.change(sortDropdown, { target: { value: 'newest' } })
expect(localDb.storage.setSort).toHaveBeenCalledWith(mockStorageName, 'newest')
expect(itemStore.storage(mockStorageName).sortItems).toHaveBeenCalledWith('newest')
})

it('renders items when they exist in the store', async () => {
cleanup()
itemStore.items[mockStorageName] = [
{ id: '1', name: 'Item 1', quantity: 1, dateAdded: '2024-01-01', shelfLife: 1, storage: mockStorageName },
{ id: '2', name: 'Item 2', quantity: 1, dateAdded: '2024-01-02', shelfLife: 1, storage: mockStorageName },
]
itemStore.itemCounts[mockStorageName] = 2
component = render(StoragePlace, { props: { storageName: mockStorageName } })
const { getByText } = component
expect(getByText('Item 1')).toBeTruthy()
expect(getByText('Item 2')).toBeTruthy()
})

it('allows editing the storage name', async () => {
const { getByLabelText } = component
const nameElement = getByLabelText('storage name')
await fireEvent.input(nameElement, { target: { textContent: 'New Storage Name' } })
await fireEvent.keyDown(nameElement, { key: 'Enter', code: 'Enter' })
expect(itemStore.updateStorage).toHaveBeenCalledWith(mockStorageName, 'New Storage Name')
expect(localDb.storage.rename).toHaveBeenCalledWith(mockStorageName, 'New Storage Name')
})
})
Loading