feeds-fun 1.18.0 → 1.18.2
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.
- package/package.json +3 -3
- package/src/App.vue +25 -1
- package/src/components/side_pannel/CollapseButton.vue +56 -0
- package/src/css/side_panel_layout.css +4 -0
- package/src/layouts/SidePanelLayout.vue +10 -4
- package/src/logic/api.ts +5 -0
- package/src/logic/events.ts +37 -0
- package/src/logic/marketing.ts +54 -0
- package/src/logic/settings.ts +8 -0
- package/src/logic/tagsFilterState.ts +11 -0
- package/src/main.ts +4 -0
- package/src/stores/globalSettings.ts +5 -1
- package/src/views/NewsView.vue +5 -0
- package/src/views/RulesView.vue +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feeds-fun",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.2",
|
|
4
4
|
"author": "Aliaksei Yaletski (Tiendil) <a.eletsky@gmail.com> (https://tiendil.org/)",
|
|
5
5
|
"description": "Frontend for the Feeds Fun — web-based news reader",
|
|
6
6
|
"keywords": [
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"@vueuse/core": "^11.2.0",
|
|
37
37
|
"axios": "^1.7.7",
|
|
38
38
|
"dompurify": "^3.1.7",
|
|
39
|
+
"lodash": "^4.17.21",
|
|
39
40
|
"pinia": "^2.2.6",
|
|
40
41
|
"set-interval-async": "^3.0.3",
|
|
41
42
|
"supertokens-web-js": "^0.5.0",
|
|
42
43
|
"vue": "^3.5.12",
|
|
43
|
-
"vue-router": "^4.4.5"
|
|
44
|
-
"lodash": "^4.17.21"
|
|
44
|
+
"vue-router": "^4.4.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rushstack/eslint-patch": "^1.10.4",
|
package/src/App.vue
CHANGED
|
@@ -2,7 +2,31 @@
|
|
|
2
2
|
<router-view />
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
|
-
<script setup lang="ts"
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import {onMounted} from "vue";
|
|
7
|
+
import {watchEffect} from "vue";
|
|
8
|
+
import {useRoute, useRouter} from "vue-router";
|
|
9
|
+
import {StorageSerializers, useStorage} from "@vueuse/core";
|
|
10
|
+
import {useGlobalState} from "@/stores/globalState";
|
|
11
|
+
import * as marketing from "@/logic/marketing";
|
|
12
|
+
import * as events from "@/logic/events";
|
|
13
|
+
|
|
14
|
+
const route = useRoute();
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const utmStorage = useStorage("ffun_utm", null, undefined, {serializer: StorageSerializers.object});
|
|
17
|
+
const globalState = useGlobalState();
|
|
18
|
+
|
|
19
|
+
watchEffect(() => {
|
|
20
|
+
marketing.processUTM(route, router, utmStorage);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
watchEffect(async () => {
|
|
24
|
+
if (utmStorage.value && globalState.isLoggedIn && Object.keys(utmStorage.value).length > 0) {
|
|
25
|
+
await events.trackUtm(utmStorage.value);
|
|
26
|
+
utmStorage.value = null;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
6
30
|
|
|
7
31
|
<style scoped>
|
|
8
32
|
.container {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a
|
|
3
|
+
href="#"
|
|
4
|
+
class="ffun-page-header-link text-2xl align-middle flex-none pb-0 relative"
|
|
5
|
+
:title="title"
|
|
6
|
+
@click.prevent="onClick">
|
|
7
|
+
<i
|
|
8
|
+
v-if="globalSettings.showSidebar"
|
|
9
|
+
class="ti ti-layout-sidebar-left-collapse"></i>
|
|
10
|
+
<i
|
|
11
|
+
v-else
|
|
12
|
+
class="ti ti-layout-sidebar-left-expand"></i>
|
|
13
|
+
|
|
14
|
+
<span
|
|
15
|
+
v-if="showPoint"
|
|
16
|
+
class="absolute top-0 right-0 h-2 w-2 rounded-full bg-red-500 border border-white"></span>
|
|
17
|
+
</a>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script lang="ts" setup>
|
|
21
|
+
import {ref, computed, useSlots, onMounted, watch, watchEffect, inject} from "vue";
|
|
22
|
+
import {useRouter, RouterLink, RouterView} from "vue-router";
|
|
23
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
24
|
+
import {useGlobalState} from "@/stores/globalState";
|
|
25
|
+
import {useSupertokens} from "@/stores/supertokens";
|
|
26
|
+
import * as asserts from "@/logic/asserts";
|
|
27
|
+
import * as events from "@/logic/events";
|
|
28
|
+
import * as e from "@/logic/enums";
|
|
29
|
+
import * as settings from "@/logic/settings";
|
|
30
|
+
|
|
31
|
+
const globalSettings = useGlobalSettingsStore();
|
|
32
|
+
|
|
33
|
+
const eventsView = inject<events.EventsViewName>("eventsViewName");
|
|
34
|
+
|
|
35
|
+
asserts.defined(eventsView);
|
|
36
|
+
|
|
37
|
+
const title = computed(() => {
|
|
38
|
+
return globalSettings.showSidebar ? "Hide sidebar" : "Show sidebar";
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const showPoint = computed(() => {
|
|
42
|
+
return !globalSettings.showSidebar && globalSettings.showSidebarPoint;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function onClick() {
|
|
46
|
+
globalSettings.showSidebar = !globalSettings.showSidebar;
|
|
47
|
+
|
|
48
|
+
asserts.defined(eventsView);
|
|
49
|
+
|
|
50
|
+
events.sidebarStateChanged({
|
|
51
|
+
view: eventsView,
|
|
52
|
+
subEvent: globalSettings.showSidebar ? "show" : "hide",
|
|
53
|
+
source: "top_sidebar_button"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ffun-side-panel-layout">
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<div
|
|
4
|
+
v-if="globalSettings.showSidebar"
|
|
5
|
+
class="ffun-side-panel">
|
|
6
|
+
<div class="ffun-page-header pr-0 mr-0 flex min-w-full">
|
|
7
|
+
<div class="ffun-page-header-title grow">
|
|
6
8
|
<slot name="main-header"></slot>
|
|
7
9
|
</div>
|
|
10
|
+
|
|
11
|
+
<side-panel-collapse-button />
|
|
8
12
|
</div>
|
|
9
13
|
|
|
10
14
|
<hr />
|
|
@@ -49,6 +53,8 @@
|
|
|
49
53
|
<div class="ffun-body-panel">
|
|
50
54
|
<div class="ffun-page-header">
|
|
51
55
|
<div class="ffun-page-header-left-block">
|
|
56
|
+
<side-panel-collapse-button v-if="!globalSettings.showSidebar" />
|
|
57
|
+
|
|
52
58
|
<a
|
|
53
59
|
v-if="homeButton"
|
|
54
60
|
:href="router.resolve({name: 'main', params: {}}).href"
|
|
@@ -92,7 +98,7 @@
|
|
|
92
98
|
</div>
|
|
93
99
|
</div>
|
|
94
100
|
|
|
95
|
-
<hr class="my-2 border-slate-400" />
|
|
101
|
+
<hr class="mx-4 my-2 border-slate-400" />
|
|
96
102
|
|
|
97
103
|
<main class="mb-4 px-4 min-h-screen">
|
|
98
104
|
<slot></slot>
|
package/src/logic/api.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as _ from "lodash";
|
|
|
2
2
|
import axios, {AxiosError} from "axios";
|
|
3
3
|
import * as t from "@/logic/types";
|
|
4
4
|
import type * as e from "@/logic/enums";
|
|
5
|
+
import * as settings from "@/logic/settings";
|
|
5
6
|
|
|
6
7
|
const ENTRY_POINT = "/api";
|
|
7
8
|
|
|
@@ -331,5 +332,9 @@ export async function getInfo() {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
export async function trackEvent(data: {[key: string]: string | number | null}) {
|
|
335
|
+
if (!settings.trackEvents) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
334
339
|
await post({url: API_TRACK_EVENT, data: {event: data}});
|
|
335
340
|
}
|
package/src/logic/events.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type EventsViewName =
|
|
|
15
15
|
| "collections";
|
|
16
16
|
export type TagChangeSource = "tags_filter" | "entry_record" | "rule_record";
|
|
17
17
|
|
|
18
|
+
export type SidebarVisibilityChangeEvent = "hide" | "show";
|
|
19
|
+
export type SidebarVisibilityChangeSource = "top_sidebar_button";
|
|
20
|
+
|
|
18
21
|
export async function newsLinkOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
|
|
19
22
|
await api.trackEvent({
|
|
20
23
|
name: "news_link_opened",
|
|
@@ -39,6 +42,23 @@ export async function socialLinkClicked({linkType, view}: {linkType: string; vie
|
|
|
39
42
|
});
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
export async function sidebarStateChanged({
|
|
46
|
+
subEvent,
|
|
47
|
+
view,
|
|
48
|
+
source
|
|
49
|
+
}: {
|
|
50
|
+
subEvent: SidebarVisibilityChangeEvent;
|
|
51
|
+
view: EventsViewName;
|
|
52
|
+
source: SidebarVisibilityChangeSource;
|
|
53
|
+
}) {
|
|
54
|
+
await api.trackEvent({
|
|
55
|
+
name: "sidebar_state_changed",
|
|
56
|
+
view: view,
|
|
57
|
+
sub_event: subEvent,
|
|
58
|
+
source: source
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
export async function tagStateChanged({
|
|
43
63
|
tag,
|
|
44
64
|
fromState,
|
|
@@ -63,3 +83,20 @@ export async function tagStateChanged({
|
|
|
63
83
|
source: source
|
|
64
84
|
});
|
|
65
85
|
}
|
|
86
|
+
|
|
87
|
+
export async function trackUtm({
|
|
88
|
+
utm_source,
|
|
89
|
+
utm_medium,
|
|
90
|
+
utm_campaign
|
|
91
|
+
}: {
|
|
92
|
+
utm_source: string;
|
|
93
|
+
utm_medium: string;
|
|
94
|
+
utm_campaign: string;
|
|
95
|
+
}) {
|
|
96
|
+
await api.trackEvent({
|
|
97
|
+
name: "user_utm",
|
|
98
|
+
utm_source: utm_source,
|
|
99
|
+
utm_medium: utm_medium,
|
|
100
|
+
utm_campaign: utm_campaign
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {useRoute, useRouter} from "vue-router";
|
|
2
|
+
import type {RouteLocationNormalizedLoaded, Router} from "vue-router";
|
|
3
|
+
import * as settings from "@/logic/settings";
|
|
4
|
+
|
|
5
|
+
export function processUTM(route: RouteLocationNormalizedLoaded, router: Router, utmStorage: any) {
|
|
6
|
+
const utmParams = ["utm_source", "utm_medium", "utm_campaign"];
|
|
7
|
+
|
|
8
|
+
// extract UTM parameters from the URL
|
|
9
|
+
const utmData = utmParams.reduce(
|
|
10
|
+
(acc, param) => {
|
|
11
|
+
const value = route.query[param];
|
|
12
|
+
|
|
13
|
+
if (!value) {
|
|
14
|
+
return acc;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
if (value[0]) {
|
|
19
|
+
acc[param] = value[0];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return acc;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value) {
|
|
26
|
+
acc[param] = value;
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return acc;
|
|
31
|
+
},
|
|
32
|
+
{} as Record<string, string>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// remove UTM parameters from the URL if they exist
|
|
36
|
+
if (Object.keys(utmData).length > 0) {
|
|
37
|
+
const newQuery = {...route.query};
|
|
38
|
+
|
|
39
|
+
utmParams.forEach((param) => {
|
|
40
|
+
if (newQuery[param]) {
|
|
41
|
+
delete newQuery[param];
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.replace({query: newQuery});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// store UTM in local storage
|
|
49
|
+
if (Object.keys(utmData).length == 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
utmStorage.value = utmData;
|
|
54
|
+
}
|
package/src/logic/settings.ts
CHANGED
|
@@ -22,6 +22,10 @@ export const plausibleEnabled = import.meta.env.VITE_FFUN_PLAUSIBLE_ENABLED == "
|
|
|
22
22
|
export const plausibleDomain = import.meta.env.VITE_FFUN_PLAUSIBLE_DOMAIN || "localhost";
|
|
23
23
|
export const plausibleScript = import.meta.env.VITE_FFUN_PLAUSIBLE_SCRIPT || "";
|
|
24
24
|
|
|
25
|
+
export const trackEvents = import.meta.env.VITE_FFUN_TRACK_EVENTS == "true" || false;
|
|
26
|
+
|
|
27
|
+
export const utmLifetime = import.meta.env.VITE_FFUN_UTM_LIFETIME || 7; // days
|
|
28
|
+
|
|
25
29
|
console.log("settings.appName", appName);
|
|
26
30
|
console.log("settings.appDomain", appDomain);
|
|
27
31
|
console.log("settings.appPort", appPort);
|
|
@@ -40,3 +44,7 @@ console.log("settings.redditSubreddit", redditSubreddit);
|
|
|
40
44
|
console.log("settings.plausibleEnabled", plausibleEnabled);
|
|
41
45
|
console.log("settings.plausibleDomain", plausibleDomain);
|
|
42
46
|
console.log("settings.plausibleScript", plausibleScript);
|
|
47
|
+
|
|
48
|
+
console.log("settings.trackEvents", trackEvents);
|
|
49
|
+
|
|
50
|
+
console.log("settings.utmLifetime", utmLifetime);
|
|
@@ -161,3 +161,14 @@ export function setSyncingTagsWithRoute({tagsStates, route, router}: {tagsStates
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// must be called synchoronously from the view
|
|
166
|
+
export function setSyncingTagsSidebarPoint({tagsStates, globalSettings}: {tagsStates: Storage; globalSettings: any}) {
|
|
167
|
+
watch(
|
|
168
|
+
tagsStates,
|
|
169
|
+
() => {
|
|
170
|
+
globalSettings.showSidebarPoint = tagsStates.hasSelectedTags;
|
|
171
|
+
},
|
|
172
|
+
{immediate: true}
|
|
173
|
+
);
|
|
174
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -67,6 +67,8 @@ import MainNewsTitle from "./components/main/NewsTitle.vue";
|
|
|
67
67
|
import MainHeaderLine from "./components/main/HeaderLine.vue";
|
|
68
68
|
import MainBlock from "./components/main/Block.vue";
|
|
69
69
|
|
|
70
|
+
import SidePanelCollapseButton from "./components/side_pannel/CollapseButton.vue";
|
|
71
|
+
|
|
70
72
|
import WideLayout from "./layouts/WideLayout.vue";
|
|
71
73
|
import SidePanelLayout from "./layouts/SidePanelLayout.vue";
|
|
72
74
|
|
|
@@ -137,6 +139,8 @@ app.component("MainNewsTitle", MainNewsTitle);
|
|
|
137
139
|
app.component("MainHeaderLine", MainHeaderLine);
|
|
138
140
|
app.component("MainBlock", MainBlock);
|
|
139
141
|
|
|
142
|
+
app.component("SidePanelCollapseButton", SidePanelCollapseButton);
|
|
143
|
+
|
|
140
144
|
app.component("WideLayout", WideLayout);
|
|
141
145
|
app.component("SidePanelLayout", SidePanelLayout);
|
|
142
146
|
|
|
@@ -12,6 +12,8 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
|
12
12
|
// General
|
|
13
13
|
const mainPanelMode = ref(e.MainPanelMode.Entries);
|
|
14
14
|
const dataVersion = ref(0);
|
|
15
|
+
const showSidebar = ref(true);
|
|
16
|
+
const showSidebarPoint = ref(false);
|
|
15
17
|
|
|
16
18
|
// Entries
|
|
17
19
|
const lastEntriesPeriod = ref(e.LastEntriesPeriod.Day3);
|
|
@@ -70,6 +72,8 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
|
70
72
|
info,
|
|
71
73
|
feedsOrder,
|
|
72
74
|
failedFeedsFirst,
|
|
73
|
-
rulesOrder
|
|
75
|
+
rulesOrder,
|
|
76
|
+
showSidebar,
|
|
77
|
+
showSidebarPoint
|
|
74
78
|
};
|
|
75
79
|
});
|
package/src/views/NewsView.vue
CHANGED
|
@@ -102,6 +102,11 @@
|
|
|
102
102
|
router
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
tagsFilterState.setSyncingTagsSidebarPoint({
|
|
106
|
+
tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
|
|
107
|
+
globalSettings
|
|
108
|
+
});
|
|
109
|
+
|
|
105
110
|
globalSettings.mainPanelMode = e.MainPanelMode.Entries;
|
|
106
111
|
|
|
107
112
|
globalSettings.updateDataVersion();
|
package/src/views/RulesView.vue
CHANGED
|
@@ -56,13 +56,18 @@
|
|
|
56
56
|
provide("tagsStates", tagsStates);
|
|
57
57
|
provide("eventsViewName", "rules");
|
|
58
58
|
|
|
59
|
+
const globalSettings = useGlobalSettingsStore();
|
|
60
|
+
|
|
59
61
|
tagsFilterState.setSyncingTagsWithRoute({
|
|
60
62
|
tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
|
|
61
63
|
route,
|
|
62
64
|
router
|
|
63
65
|
});
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
tagsFilterState.setSyncingTagsSidebarPoint({
|
|
68
|
+
tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
|
|
69
|
+
globalSettings
|
|
70
|
+
});
|
|
66
71
|
|
|
67
72
|
globalSettings.mainPanelMode = e.MainPanelMode.Rules;
|
|
68
73
|
|