feeds-fun 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/public/news-filtering-example.png +0 -0
  3. package/src/components/EntriesList.vue +11 -0
  4. package/src/components/EntryForList.vue +17 -6
  5. package/src/components/RuleForList.vue +2 -2
  6. package/src/components/RulesList.vue +18 -9
  7. package/src/components/TokensCost.vue +0 -2
  8. package/src/components/collections/DetailedItem.vue +9 -0
  9. package/src/components/collections/PublicIntro.vue +63 -0
  10. package/src/components/collections/PublicSelector.vue +43 -0
  11. package/src/components/main/Block.vue +1 -1
  12. package/src/components/main/Item.vue +1 -1
  13. package/src/components/notifications/LoadedOldNews.vue +47 -0
  14. package/src/components/page_header/ExternalLinks.vue +11 -6
  15. package/src/components/tags/EntryTag.vue +4 -3
  16. package/src/components/tags/FilterTag.vue +6 -3
  17. package/src/components/tags/RuleTag.vue +4 -3
  18. package/src/components/tags/TagsFilter.vue +17 -5
  19. package/src/css/panels.css +5 -5
  20. package/src/layouts/SidePanelLayout.vue +41 -22
  21. package/src/layouts/WideLayout.vue +0 -4
  22. package/src/logic/api.ts +23 -0
  23. package/src/logic/enums.ts +28 -12
  24. package/src/logic/events.ts +36 -8
  25. package/src/logic/tagsFilterState.ts +50 -1
  26. package/src/logic/types.ts +15 -2
  27. package/src/logic/utils.ts +104 -1
  28. package/src/main.ts +6 -0
  29. package/src/router/index.ts +16 -3
  30. package/src/stores/collections.ts +17 -1
  31. package/src/stores/entries.ts +154 -19
  32. package/src/stores/globalSettings.ts +6 -7
  33. package/src/views/AuthView.vue +3 -1
  34. package/src/views/CollectionsView.vue +13 -2
  35. package/src/views/DiscoveryView.vue +3 -1
  36. package/src/views/FeedsView.vue +3 -1
  37. package/src/views/MainView.vue +21 -2
  38. package/src/views/NewsView.vue +32 -98
  39. package/src/views/PublicCollectionView.vue +237 -0
  40. package/src/views/RulesView.vue +26 -29
  41. package/src/views/SettingsView.vue +3 -1
@@ -49,29 +49,41 @@
49
49
  <div class="ffun-body-panel">
50
50
  <div class="ffun-page-header">
51
51
  <div class="ffun-page-header-left-block">
52
- <template
53
- v-for="[mode, props] of e.MainPanelModeProperties"
54
- :key="mode">
55
- <a
56
- v-if="globalSettings.mainPanelMode !== mode"
57
- :href="router.resolve({name: mode, params: {}}).href"
58
- class="ffun-page-header-link"
59
- @click.prevent="router.push({name: mode, params: {}})">
60
- {{ props.text }}
61
- </a>
62
-
63
- <span
64
- class="ffun-page-header-link-disabled"
65
- v-else
66
- >{{ props.text }}</span
67
- >
52
+ <a
53
+ v-if="homeButton"
54
+ :href="router.resolve({name: 'main', params: {}}).href"
55
+ class="ffun-page-header-link"
56
+ >Home</a
57
+ >
58
+
59
+ <template v-if="globalState.isLoggedIn">
60
+ <template
61
+ v-for="[mode, props] of e.MainPanelModeProperties"
62
+ :key="mode">
63
+ <template v-if="props.showInMenu">
64
+ <a
65
+ v-if="globalSettings.mainPanelMode !== mode"
66
+ :href="router.resolve({name: mode, params: {}}).href"
67
+ class="ffun-page-header-link"
68
+ @click.prevent="router.push({name: mode, params: {}})">
69
+ {{ props.text }}
70
+ </a>
71
+
72
+ <span
73
+ class="ffun-page-header-link-disabled"
74
+ v-else
75
+ >{{ props.text }}</span
76
+ >
77
+ </template>
78
+ </template>
68
79
  </template>
69
80
 
70
- <page-header-external-links :show-api="true" />
81
+ <page-header-external-links :show-api="globalState.isLoggedIn" />
71
82
  </div>
72
83
 
73
84
  <div class="ffun-page-header-right-block">
74
85
  <a
86
+ v-if="globalState.isLoggedIn"
75
87
  href="#"
76
88
  class="ffun-page-header-link"
77
89
  @click.prevent="logout()"
@@ -110,10 +122,14 @@
110
122
  const router = useRouter();
111
123
  const slots = useSlots();
112
124
 
113
- const properties = withDefaults(defineProps<{reloadButton?: boolean; loginRequired?: boolean}>(), {
114
- reloadButton: true,
115
- loginRequired: true
116
- });
125
+ const properties = withDefaults(
126
+ defineProps<{reloadButton?: boolean; loginRequired?: boolean; homeButton?: boolean}>(),
127
+ {
128
+ reloadButton: true,
129
+ loginRequired: true,
130
+ homeButton: false
131
+ }
132
+ );
117
133
 
118
134
  async function logout() {
119
135
  if (settings.authMode === settings.AuthMode.SingleUser) {
@@ -122,7 +138,10 @@
122
138
  }
123
139
 
124
140
  await supertokens.logout();
125
- router.push({name: "main", params: {}});
141
+
142
+ if (properties.loginRequired) {
143
+ router.push({name: "main", params: {}});
144
+ }
126
145
  }
127
146
 
128
147
  const hasSideFooter = computed(() => {
@@ -1,9 +1,5 @@
1
1
  <template>
2
- <!-- <div class="flex justify-center h-screen"> -->
3
- <!-- <div class="flex flex-col justify-start items-center text-center p-4"> -->
4
2
  <slot></slot>
5
- <!-- </div> -->
6
- <!-- </div> -->
7
3
  </template>
8
4
 
9
5
  <script lang="ts" setup></script>
package/src/logic/api.ts CHANGED
@@ -7,6 +7,7 @@ const ENTRY_POINT = "/api";
7
7
 
8
8
  const API_GET_FEEDS = `${ENTRY_POINT}/get-feeds`;
9
9
  const API_GET_LAST_ENTRIES = `${ENTRY_POINT}/get-last-entries`;
10
+ const API_GET_LAST_COLLECTION_ENTRIES = `${ENTRY_POINT}/get-last-collection-entries`;
10
11
  const API_GET_ENTRIES_BY_IDS = `${ENTRY_POINT}/get-entries-by-ids`;
11
12
  const API_CREATE_OR_UPDATE_RULE = `${ENTRY_POINT}/create-or-update-rule`;
12
13
 
@@ -83,6 +84,28 @@ export async function getLastEntries({period}: {period: number}) {
83
84
  return entries;
84
85
  }
85
86
 
87
+ export async function getLastCollectionEntries({
88
+ period,
89
+ collectionSlug
90
+ }: {
91
+ period: number;
92
+ collectionSlug: t.CollectionSlug | null;
93
+ }) {
94
+ const response = await post({
95
+ url: API_GET_LAST_COLLECTION_ENTRIES,
96
+ data: {period: period, collectionSlug: collectionSlug}
97
+ });
98
+
99
+ const entries = [];
100
+
101
+ for (let rawEntry of response.entries) {
102
+ const entry = t.entryFromJSON(rawEntry, response.tagsMapping);
103
+ entries.push(entry);
104
+ }
105
+
106
+ return entries;
107
+ }
108
+
86
109
  export async function getEntriesByIds({ids}: {ids: t.EntryId[]}) {
87
110
  const response = await post({
88
111
  url: API_GET_ENTRIES_BY_IDS,
@@ -10,16 +10,23 @@ export enum MainPanelMode {
10
10
  Rules = "rules",
11
11
  Discovery = "discovery",
12
12
  Collections = "collections",
13
+ PublicCollection = "public-collection",
13
14
  Settings = "settings"
14
15
  }
15
16
 
16
- export const MainPanelModeProperties = new Map<MainPanelMode, {text: string}>([
17
- [MainPanelMode.Entries, {text: "News"}],
18
- [MainPanelMode.Feeds, {text: "Feeds"}],
19
- [MainPanelMode.Rules, {text: "Rules"}],
20
- [MainPanelMode.Discovery, {text: "Discovery"}],
21
- [MainPanelMode.Collections, {text: "Collections"}],
22
- [MainPanelMode.Settings, {text: "Settings"}]
17
+ export type MainPanelModeProperty = {
18
+ readonly text: string;
19
+ readonly showInMenu: boolean;
20
+ };
21
+
22
+ export const MainPanelModeProperties = new Map<MainPanelMode, MainPanelModeProperty>([
23
+ [MainPanelMode.Entries, {text: "News", showInMenu: true}],
24
+ [MainPanelMode.Feeds, {text: "Feeds", showInMenu: true}],
25
+ [MainPanelMode.Rules, {text: "Rules", showInMenu: true}],
26
+ [MainPanelMode.Discovery, {text: "Discovery", showInMenu: true}],
27
+ [MainPanelMode.Collections, {text: "Collections", showInMenu: true}],
28
+ [MainPanelMode.PublicCollection, {text: "Public Collection", showInMenu: false}],
29
+ [MainPanelMode.Settings, {text: "Settings", showInMenu: true}]
23
30
  ]);
24
31
 
25
32
  export enum LastEntriesPeriod {
@@ -38,8 +45,13 @@ export enum LastEntriesPeriod {
38
45
  AllTime = "alltime"
39
46
  }
40
47
 
48
+ export type LastEntriesPeriodProperty = {
49
+ readonly text: string;
50
+ readonly seconds: number;
51
+ };
52
+
41
53
  // Map preserves the order of the keys
42
- export const LastEntriesPeriodProperties = new Map<LastEntriesPeriod, {text: string; seconds: number}>([
54
+ export const LastEntriesPeriodProperties = new Map<LastEntriesPeriod, LastEntriesPeriodProperty>([
43
55
  [LastEntriesPeriod.Hour1, {text: "1 hour", seconds: c.hour}],
44
56
  [LastEntriesPeriod.Hour3, {text: "3 hours", seconds: 3 * c.hour}],
45
57
  [LastEntriesPeriod.Hour6, {text: "6 hours", seconds: 6 * c.hour}],
@@ -63,10 +75,14 @@ export enum EntriesOrder {
63
75
  Cataloged = "cataloged"
64
76
  }
65
77
 
66
- export const EntriesOrderProperties = new Map<
67
- EntriesOrder,
68
- {text: string; orderField: string; timeField: string; direction: number}
69
- >([
78
+ export type EntriesOrderProperty = {
79
+ readonly text: string;
80
+ readonly orderField: string;
81
+ readonly timeField: string;
82
+ readonly direction: number;
83
+ };
84
+
85
+ export const EntriesOrderProperties = new Map<EntriesOrder, EntriesOrderProperty>([
70
86
  [EntriesOrder.Score, {text: "score", orderField: "score", timeField: "catalogedAt", direction: 1}],
71
87
  [EntriesOrder.ScoreToZero, {text: "score ~ 0", orderField: "scoreToZero", timeField: "catalogedAt", direction: 1}],
72
88
  [EntriesOrder.ScoreBackward, {text: "score backward", orderField: "score", timeField: "catalogedAt", direction: -1}],
@@ -1,37 +1,65 @@
1
1
  import * as api from "@/logic/api";
2
2
  import type * as t from "@/logic/types";
3
+ import {inject} from "vue";
3
4
  import type {State as TagState} from "@/logic/tagsFilterState";
4
5
 
5
- export type TagChangeSource = "news_tags_filter" | "rules_tags_filter" | "entry_record" | "rule_record";
6
+ export type EventsViewName =
7
+ | "news"
8
+ | "rules"
9
+ | "public_collections"
10
+ | "auth"
11
+ | "settings"
12
+ | "discovery"
13
+ | "feeds"
14
+ | "main"
15
+ | "collections";
16
+ export type TagChangeSource = "tags_filter" | "entry_record" | "rule_record";
6
17
 
7
- export async function newsLinkOpened({entryId}: {entryId: t.EntryId}) {
8
- await api.trackEvent({name: "news_link_opened", entry_id: entryId});
18
+ export async function newsLinkOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
19
+ await api.trackEvent({
20
+ name: "news_link_opened",
21
+ view: view,
22
+ entry_id: entryId
23
+ });
9
24
  }
10
25
 
11
- export async function newsBodyOpened({entryId}: {entryId: t.EntryId}) {
12
- await api.trackEvent({name: "news_body_opened", entry_id: entryId});
26
+ export async function newsBodyOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
27
+ await api.trackEvent({
28
+ name: "news_body_opened",
29
+ view: view,
30
+ entry_id: entryId
31
+ });
13
32
  }
14
33
 
15
- export async function socialLinkClicked({linkType}: {linkType: string}) {
16
- await api.trackEvent({name: "social_link_clicked", link_type: linkType});
34
+ export async function socialLinkClicked({linkType, view}: {linkType: string; view: EventsViewName}) {
35
+ await api.trackEvent({
36
+ name: "social_link_clicked",
37
+ view: view,
38
+ link_type: linkType
39
+ });
17
40
  }
18
41
 
19
42
  export async function tagStateChanged({
20
43
  tag,
21
44
  fromState,
22
45
  toState,
23
- source
46
+ source,
47
+ view
24
48
  }: {
25
49
  tag: string;
26
50
  fromState: TagState;
27
51
  toState: TagState;
28
52
  source: TagChangeSource;
53
+ view: EventsViewName;
29
54
  }) {
55
+ // const eventsView = inject<events.EventViewName>("eventsViewName");
56
+
30
57
  await api.trackEvent({
31
58
  name: "tag_filter_state_changed",
32
59
  tag: tag,
33
60
  from_state: fromState,
34
61
  to_state: toState,
62
+ view: view,
35
63
  source: source
36
64
  });
37
65
  }
@@ -1,5 +1,6 @@
1
- import {ref, computed, reactive} from "vue";
1
+ import {ref, computed, reactive, watch} from "vue";
2
2
  import type {ComputedRef} from "vue";
3
+ import _ from "lodash";
3
4
 
4
5
  export type State = "required" | "excluded" | "none";
5
6
 
@@ -102,6 +103,34 @@ export class Storage {
102
103
  return report;
103
104
  }
104
105
 
106
+ tagsForUrl() {
107
+ const selectedTags = Object.keys(this.selectedTags).sort();
108
+
109
+ const tags = [];
110
+
111
+ for (const tag of selectedTags) {
112
+ if (this.requiredTags[tag]) {
113
+ tags.push(tag);
114
+ } else {
115
+ tags.push(`-${tag}`);
116
+ }
117
+ }
118
+
119
+ return tags;
120
+ }
121
+
122
+ setTagsFromUrl(tags: string[]) {
123
+ this.clear();
124
+
125
+ for (const tag of tags) {
126
+ if (tag.startsWith("-")) {
127
+ this.excludedTags[tag.slice(1)] = true;
128
+ } else {
129
+ this.requiredTags[tag] = true;
130
+ }
131
+ }
132
+ }
133
+
105
134
  clear() {
106
135
  Object.keys(this.requiredTags).forEach((key) => {
107
136
  delete this.requiredTags[key];
@@ -112,3 +141,23 @@ export class Storage {
112
141
  });
113
142
  }
114
143
  }
144
+
145
+ // must be called synchoronously from the view
146
+ export function setSyncingTagsWithRoute({tagsStates, route, router}: {tagsStates: Storage; route: any; router: any}) {
147
+ if (!route.params.tags) {
148
+ tagsStates.setTagsFromUrl([]);
149
+ } else {
150
+ tagsStates.setTagsFromUrl(route.params.tags);
151
+ }
152
+
153
+ watch(tagsStates, () => {
154
+ const newParams = _.clone(route.params);
155
+ newParams.tags = tagsStates.tagsForUrl();
156
+
157
+ router.push({
158
+ replace: true,
159
+ name: route.name,
160
+ params: newParams
161
+ });
162
+ });
163
+ }
@@ -24,6 +24,12 @@ export function toCollectionId(id: string): CollectionId {
24
24
  return id as CollectionId;
25
25
  }
26
26
 
27
+ export type CollectionSlug = string & {readonly __brand: unique symbol};
28
+
29
+ export function toCollectionSlug(slug: string): CollectionSlug {
30
+ return slug as CollectionSlug;
31
+ }
32
+
27
33
  export type URL = string & {readonly __brand: unique symbol};
28
34
 
29
35
  export function toURL(url: string): URL {
@@ -233,7 +239,7 @@ export type Rule = {
233
239
  readonly id: RuleId;
234
240
  readonly requiredTags: string[];
235
241
  readonly excludedTags: string[];
236
- readonly allTags: string[];
242
+ readonly tags: string[];
237
243
  readonly score: number;
238
244
  readonly createdAt: Date;
239
245
  readonly updatedAt: Date;
@@ -261,7 +267,7 @@ export function ruleFromJSON({
261
267
  id: toRuleId(id),
262
268
  requiredTags: requiredTags,
263
269
  excludedTags: excludedTags,
264
- allTags: requiredTags.concat(excludedTags).sort(),
270
+ tags: requiredTags.concat(excludedTags).sort(),
265
271
  score: score,
266
272
  createdAt: new Date(createdAt),
267
273
  updatedAt: new Date(updatedAt)
@@ -411,6 +417,7 @@ export function resourceHistoryRecordFromJSON({
411
417
 
412
418
  export class Collection {
413
419
  readonly id: CollectionId;
420
+ readonly slug: CollectionSlug;
414
421
  readonly guiOrder: number;
415
422
  readonly name: string;
416
423
  readonly description: string;
@@ -419,6 +426,7 @@ export class Collection {
419
426
 
420
427
  constructor({
421
428
  id,
429
+ slug,
422
430
  guiOrder,
423
431
  name,
424
432
  description,
@@ -426,6 +434,7 @@ export class Collection {
426
434
  showOnMain
427
435
  }: {
428
436
  id: CollectionId;
437
+ slug: CollectionSlug;
429
438
  guiOrder: number;
430
439
  name: string;
431
440
  description: string;
@@ -433,6 +442,7 @@ export class Collection {
433
442
  showOnMain: boolean;
434
443
  }) {
435
444
  this.id = id;
445
+ this.slug = slug;
436
446
  this.guiOrder = guiOrder;
437
447
  this.name = name;
438
448
  this.description = description;
@@ -443,6 +453,7 @@ export class Collection {
443
453
 
444
454
  export function collectionFromJSON({
445
455
  id,
456
+ slug,
446
457
  guiOrder,
447
458
  name,
448
459
  description,
@@ -450,6 +461,7 @@ export function collectionFromJSON({
450
461
  showOnMain
451
462
  }: {
452
463
  id: string;
464
+ slug: string;
453
465
  guiOrder: number;
454
466
  name: string;
455
467
  description: string;
@@ -458,6 +470,7 @@ export function collectionFromJSON({
458
470
  }): Collection {
459
471
  return {
460
472
  id: toCollectionId(id),
473
+ slug: toCollectionSlug(slug),
461
474
  guiOrder: guiOrder,
462
475
  name: name,
463
476
  description: description,
@@ -1,4 +1,5 @@
1
1
  import _ from "lodash";
2
+ import type * as t from "@/logic/types";
2
3
  import DOMPurify from "dompurify";
3
4
 
4
5
  export function timeSince(date: Date) {
@@ -64,7 +65,6 @@ export function faviconForUrl(url: string): string | null {
64
65
  const parsedUrl = new URL(url);
65
66
  return `${parsedUrl.protocol}//${parsedUrl.host}/favicon.ico`;
66
67
  } catch (error) {
67
- console.error("Invalid URL:", error);
68
68
  return null;
69
69
  }
70
70
  }
@@ -96,3 +96,106 @@ export function purifyBody({raw, default_}: {raw: string | null; default_: strin
96
96
 
97
97
  return body;
98
98
  }
99
+
100
+ export function chooseTagByUsage({
101
+ tagsCount,
102
+ border,
103
+ exclude
104
+ }: {
105
+ tagsCount: {[key: string]: number};
106
+ border: number;
107
+ exclude: string[];
108
+ }) {
109
+ if (Object.keys(tagsCount).length === 0) {
110
+ return null;
111
+ }
112
+
113
+ if (!exclude) {
114
+ exclude = [];
115
+ }
116
+
117
+ const tags = _.toPairs(tagsCount).sort((a, b) => {
118
+ if (a[1] === b[1]) {
119
+ return a[0].localeCompare(b[0]);
120
+ }
121
+
122
+ return b[1] - a[1];
123
+ });
124
+
125
+ for (let i = 0; i < tags.length; i++) {
126
+ if (exclude.includes(tags[i][0])) {
127
+ continue;
128
+ }
129
+
130
+ if (tags[i][1] < border) {
131
+ return tags[i][0];
132
+ }
133
+ }
134
+
135
+ return tags[tags.length - 1][0];
136
+ }
137
+
138
+ export function countTags(entries: t.Entry[] | t.Rule[] | null) {
139
+ if (!entries) {
140
+ return {};
141
+ }
142
+
143
+ const tagsCount: {[key: string]: number} = {};
144
+
145
+ for (const entry of entries) {
146
+ for (const tag of entry.tags) {
147
+ if (tag in tagsCount) {
148
+ tagsCount[tag] += 1;
149
+ } else {
150
+ tagsCount[tag] = 1;
151
+ }
152
+ }
153
+ }
154
+
155
+ return tagsCount;
156
+ }
157
+
158
+ export function sortIdsList<ID extends string = string>({
159
+ ids,
160
+ storage,
161
+ field,
162
+ direction
163
+ }: {
164
+ ids: ID[];
165
+ storage: {[key: string]: any};
166
+ field: string;
167
+ direction: number;
168
+ }) {
169
+ // Pre-map to avoid repeated lookups in the comparator
170
+ // required for the cases when storage is reactive
171
+ const mapped = ids.map((id) => {
172
+ // @ts-ignore
173
+ return {id, value: storage[id][field]};
174
+ });
175
+
176
+ mapped.sort((a: {id: ID; value: any}, b: {id: ID; value: any}) => {
177
+ if (a.value === null && b.value === null) {
178
+ return 0;
179
+ }
180
+
181
+ if (a.value === null) {
182
+ return 1;
183
+ }
184
+
185
+ if (b.value === null) {
186
+ return -1;
187
+ }
188
+
189
+ if (a.value < b.value) {
190
+ return direction;
191
+ }
192
+
193
+ if (a.value > b.value) {
194
+ return -direction;
195
+ }
196
+
197
+ return 0;
198
+ });
199
+
200
+ return mapped.map((x) => x.id);
201
+ }
package/src/main.ts CHANGED
@@ -38,6 +38,7 @@ import PageHeaderExternalLinks from "./components/page_header/ExternalLinks.vue"
38
38
  import NotificationsApiKey from "./components/notifications/ApiKey.vue";
39
39
  import NotificationsCreateRuleHelp from "./components/notifications/CreateRuleHelp.vue";
40
40
  import Notifications from "./components/notifications/Block.vue";
41
+ import NotificationsLoadedOldNews from "./components/notifications/LoadedOldNews.vue";
41
42
 
42
43
  import CollectionsNotification from "./components/collections/Notification.vue";
43
44
  import CollectionsWarning from "./components/collections/Warning.vue";
@@ -46,6 +47,8 @@ import CollectionsBlockItem from "./components/collections/BlockItem.vue";
46
47
  import CollectionsDetailedItem from "./components/collections/DetailedItem.vue";
47
48
  import CollectionsSubscribingProgress from "./components/collections/SubscribingProgress.vue";
48
49
  import CollectionsFeedItem from "./components/collections/FeedItem.vue";
50
+ import CollectionsPublicSelector from "./components/collections/PublicSelector.vue";
51
+ import CollectionsPublicIntro from "./components/collections/PublicIntro.vue";
49
52
 
50
53
  import ScoreSelector from "./inputs/ScoreSelector.vue";
51
54
 
@@ -105,6 +108,7 @@ app.component("PageHeaderExternalLinks", PageHeaderExternalLinks);
105
108
  app.component("NotificationsApiKey", NotificationsApiKey);
106
109
  app.component("NotificationsCreateRuleHelp", NotificationsCreateRuleHelp);
107
110
  app.component("Notifications", Notifications);
111
+ app.component("NotificationsLoadedOldNews", NotificationsLoadedOldNews);
108
112
 
109
113
  app.component("CollectionsNotification", CollectionsNotification);
110
114
  app.component("CollectionsWarning", CollectionsWarning);
@@ -113,6 +117,8 @@ app.component("CollectionsBlockItem", CollectionsBlockItem);
113
117
  app.component("CollectionsDetailedItem", CollectionsDetailedItem);
114
118
  app.component("CollectionsSubscribingProgress", CollectionsSubscribingProgress);
115
119
  app.component("CollectionsFeedItem", CollectionsFeedItem);
120
+ app.component("CollectionsPublicSelector", CollectionsPublicSelector);
121
+ app.component("CollectionsPublicIntro", CollectionsPublicIntro);
116
122
 
117
123
  app.component("ScoreSelector", ScoreSelector);
118
124
 
@@ -7,6 +7,7 @@ import RulesView from "../views/RulesView.vue";
7
7
  import DiscoveryView from "../views/DiscoveryView.vue";
8
8
  import CollectionsView from "../views/CollectionsView.vue";
9
9
  import SettingsView from "../views/SettingsView.vue";
10
+ import PublicCollectionView from "../views/PublicCollectionView.vue";
10
11
  import * as e from "@/logic/enums";
11
12
 
12
13
  // lazy view loading does not work with router.push function
@@ -31,12 +32,12 @@ const router = createRouter({
31
32
  component: FeedsView
32
33
  },
33
34
  {
34
- path: "/news",
35
+ path: "/news/:tags*",
35
36
  name: e.MainPanelMode.Entries,
36
37
  component: NewsView
37
38
  },
38
39
  {
39
- path: "/rules",
40
+ path: "/rules/:tags*",
40
41
  name: e.MainPanelMode.Rules,
41
42
  component: RulesView
42
43
  },
@@ -54,8 +55,20 @@ const router = createRouter({
54
55
  path: "/settings",
55
56
  name: e.MainPanelMode.Settings,
56
57
  component: SettingsView
58
+ },
59
+ {
60
+ path: "/show/:collectionSlug/:tags*",
61
+ name: "public-collection",
62
+ component: PublicCollectionView
63
+ },
64
+ {
65
+ path: "/:pathMatch(.*)*",
66
+ redirect: "/"
57
67
  }
58
- ]
68
+ ],
69
+ scrollBehavior(to, from, savedPosition) {
70
+ return {top: 0};
71
+ }
59
72
  });
60
73
 
61
74
  export default router;
@@ -33,6 +33,10 @@ export const useCollectionsStore = defineStore("collectionsStore", () => {
33
33
  return order;
34
34
  });
35
35
 
36
+ const collectionsOrdered = computed(() => {
37
+ return collectionsOrder.value.map((id) => collections.value[id]);
38
+ });
39
+
36
40
  async function getFeeds({collectionId}: {collectionId: t.CollectionId}) {
37
41
  if (collectionId in feeds.value) {
38
42
  return feeds.value[collectionId];
@@ -45,9 +49,21 @@ export const useCollectionsStore = defineStore("collectionsStore", () => {
45
49
  return feeds.value[collectionId];
46
50
  }
47
51
 
52
+ function getCollectionBySlug({slug}: {slug: t.CollectionSlug}) {
53
+ for (const collection of Object.values(collections.value)) {
54
+ if (collection.slug === slug) {
55
+ return collection;
56
+ }
57
+ }
58
+
59
+ return null;
60
+ }
61
+
48
62
  return {
49
63
  collections,
50
64
  collectionsOrder,
51
- getFeeds
65
+ collectionsOrdered,
66
+ getFeeds,
67
+ getCollectionBySlug
52
68
  };
53
69
  });