|
22 | 22 | import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
|
23 | 23 | import Create from 'svelte-material-icons/Plus.svelte';
|
24 | 24 |
|
25 |
| - let searchParams: Readable<URLSearchParams>; |
| 25 | + let expanded = false; |
26 | 26 |
|
27 |
| - if (building) searchParams = readable(new URLSearchParams()); |
28 |
| - else searchParams = derived(page, ($page) => $page.url.searchParams); |
| 27 | + const searchParams: Readable<URLSearchParams> = building |
| 28 | + ? readable(new URLSearchParams()) |
| 29 | + : derived(page, ($page) => $page.url.searchParams); |
29 | 30 |
|
30 | 31 | let searchTerm = $searchParams.get('s') || '';
|
| 32 | + let displayedTerm = ''; |
31 | 33 |
|
32 | 34 | $: query = createQuery(queries.announcements());
|
33 | 35 | $: tagsQuery = createQuery(queries.announcementTags());
|
34 | 36 | $: selectedTags = $searchParams.getAll('tag');
|
35 | 37 |
|
36 |
| - let expanded = false; |
37 |
| -
|
38 |
| - function filterAnnouncements( |
39 |
| - announcements: Iterable<ResponseAnnouncement>, |
40 |
| - search: string, |
41 |
| - selectedTags: string[] |
42 |
| - ): ResponseAnnouncement[] { |
43 |
| - const announcementFilter = createFilter(Array.from(announcements), { |
44 |
| - searcherOptions: { |
45 |
| - keys: ['title', 'content'] |
46 |
| - }, |
47 |
| - additionalFilter: (announcement: ResponseAnnouncement, tags: string[]): boolean => { |
48 |
| - return ( |
49 |
| - tags.length === 0 || |
50 |
| - tags.some((tag) => announcement.tags && announcement.tags.includes(tag)) |
51 |
| - ); |
52 |
| - } |
53 |
| - }); |
54 |
| -
|
55 |
| - return announcementFilter(selectedTags, search); |
56 |
| - } |
57 |
| -
|
58 |
| - // Make sure we don't have to filter the announcements after every key press |
59 |
| - let displayedTerm = ''; |
60 | 38 | const update = () => {
|
61 | 39 | displayedTerm = searchTerm;
|
62 | 40 |
|
63 | 41 | const url = new URL(window.location.href);
|
64 | 42 | url.pathname = '/announcements';
|
65 | 43 |
|
66 |
| - if (searchTerm) url.searchParams.set('s', searchTerm); |
67 |
| - else url.searchParams.delete('s'); |
| 44 | + searchTerm ? url.searchParams.set('s', searchTerm) : url.searchParams.delete('s'); |
68 | 45 | };
|
69 | 46 |
|
70 |
| - onMount(update); |
| 47 | + const archivedAnnouncements = (announcements: ResponseAnnouncement[]) => |
| 48 | + announcements.filter((a) => a.archived_at && moment(a.archived_at).isBefore(moment())); |
| 49 | + const activeAnnouncements = (announcements: ResponseAnnouncement[]) => |
| 50 | + announcements.filter((a) => !a.archived_at || moment(a.archived_at).isAfter(moment())); |
| 51 | +
|
| 52 | + const filterAnnouncements = ( |
| 53 | + announcements: ResponseAnnouncement[], |
| 54 | + search: string, |
| 55 | + tags: string[] |
| 56 | + ): ResponseAnnouncement[] => { |
| 57 | + const announcementFilter = createFilter(announcements, { |
| 58 | + searcherOptions: { keys: ['title', 'content'] }, |
| 59 | +
|
| 60 | + additionalFilter: (a: ResponseAnnouncement, tags: string[]) => |
| 61 | + tags.length === 0 || tags.some((tag) => a.tags?.includes(tag)) |
| 62 | + }); |
| 63 | +
|
| 64 | + return announcementFilter(tags, search); |
| 65 | + }; |
| 66 | +
|
| 67 | + onMount(() => { |
| 68 | + debounce(update)(); |
| 69 | + }); |
71 | 70 | </script>
|
72 | 71 |
|
73 | 72 | <div class="search">
|
|
92 | 91 | </Query>
|
93 | 92 |
|
94 | 93 | <Query {query} let:data>
|
95 |
| - <div class="cards"> |
96 |
| - {#each filterAnnouncements(data.announcements, displayedTerm, selectedTags) as announcement} |
97 |
| - {#if !announcement.archived_at || moment(announcement.archived_at).isAfter(moment())} |
98 |
| - {#key selectedTags || displayedTerm} |
99 |
| - <div in:fly={{ y: 10, easing: quintOut, duration: 750 }}> |
100 |
| - <AnnouncementCard {announcement} /> |
101 |
| - </div> |
102 |
| - {/key} |
103 |
| - {/if} |
104 |
| - {/each} |
105 |
| - </div> |
106 |
| - |
107 |
| - <div |
108 |
| - role="button" |
109 |
| - class="expand-archived" |
110 |
| - aria-expanded={expanded} |
111 |
| - class:closed={!expanded} |
112 |
| - on:click={() => (expanded = !expanded)} |
113 |
| - on:keypress={() => (expanded = !expanded)} |
114 |
| - tabindex="0" |
115 |
| - > |
116 |
| - <h4>Archived announcements</h4> |
117 |
| - |
118 |
| - <div id="arrow" style:transform={expanded ? 'rotate(0deg)' : 'rotate(-180deg)'}> |
119 |
| - <ChevronDown size="24px" color="var(--surface-six)" /> |
| 94 | + {#if activeAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)).length} |
| 95 | + <div class="cards"> |
| 96 | + {#each activeAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)) as announcement} |
| 97 | + <div in:fly={{ y: 10, easing: quintOut, duration: 750 }}> |
| 98 | + <AnnouncementCard {announcement} /> |
| 99 | + </div> |
| 100 | + {/each} |
120 | 101 | </div>
|
121 |
| - </div> |
| 102 | + {/if} |
122 | 103 |
|
123 |
| - {#if expanded} |
| 104 | + {#if archivedAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)).length} |
124 | 105 | <div
|
125 |
| - class="cards" |
126 |
| - in:slide={{ easing: quintIn, duration: 250 }} |
127 |
| - out:slide={{ easing: quintOut, duration: 250 }} |
| 106 | + role="button" |
| 107 | + class="expand-archived" |
| 108 | + aria-expanded={expanded} |
| 109 | + on:click={() => (expanded = !expanded)} |
| 110 | + on:keypress={() => (expanded = !expanded)} |
| 111 | + tabindex="0" |
128 | 112 | >
|
129 |
| - {#each filterAnnouncements(data.announcements, displayedTerm, selectedTags) as announcement} |
130 |
| - {#if announcement.archived_at && moment(announcement.archived_at).isBefore(moment())} |
131 |
| - {#key selectedTags || displayedTerm} |
132 |
| - <AnnouncementCard {announcement} /> |
133 |
| - {/key} |
134 |
| - {/if} |
135 |
| - {/each} |
| 113 | + <h4>Archived announcements</h4> |
| 114 | + |
| 115 | + <div id="arrow" style:transform={expanded ? 'rotate(-180deg)' : 'rotate(0deg)'}> |
| 116 | + <ChevronDown size="24px" color="var(--surface-six)" /> |
| 117 | + </div> |
136 | 118 | </div>
|
| 119 | + |
| 120 | + {#if expanded} |
| 121 | + <div |
| 122 | + class="cards" |
| 123 | + in:slide={{ easing: quintIn, duration: 250 }} |
| 124 | + out:slide={{ easing: quintOut, duration: 250 }} |
| 125 | + > |
| 126 | + {#each archivedAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)) as announcement} |
| 127 | + <AnnouncementCard {announcement} /> |
| 128 | + {/each} |
| 129 | + </div> |
| 130 | + {/if} |
137 | 131 | {/if}
|
138 | 132 | </Query>
|
139 | 133 | </main>
|
140 | 134 |
|
141 | 135 | <style lang="scss">
|
| 136 | + main { |
| 137 | + display: flex; |
| 138 | + flex-direction: column; |
| 139 | + gap: 1rem; |
| 140 | + } |
| 141 | +
|
142 | 142 | .expand-archived {
|
143 | 143 | display: flex;
|
144 | 144 | align-items: center;
|
145 | 145 | justify-content: space-between;
|
146 | 146 | cursor: pointer;
|
147 | 147 | user-select: none;
|
148 |
| - padding: 0rem 0.25rem; |
| 148 | + padding-inline: 0.25rem; |
149 | 149 |
|
150 | 150 | #arrow {
|
151 | 151 | height: 1.5rem;
|
|
174 | 174 | .cards {
|
175 | 175 | display: grid;
|
176 | 176 | grid-template-columns: repeat(3, 1fr);
|
177 |
| - padding: 16px 0; |
178 | 177 | width: 100%;
|
179 | 178 | gap: 16px;
|
180 | 179 |
|
|
0 commit comments