feeds-fun 1.18.2 → 1.19.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.
@@ -0,0 +1,170 @@
1
+ import "vanilla-cookieconsent/dist/cookieconsent.css";
2
+ import * as CookieConsent from "vanilla-cookieconsent";
3
+
4
+ import * as settings from "@/logic/settings";
5
+
6
+ export const plugin = {
7
+ install(app: any, pluginConfig: any): void {
8
+ app.config.globalProperties.$CookieConsent = CookieConsent;
9
+ CookieConsent.run(pluginConfig);
10
+ }
11
+ };
12
+
13
+ export function showCookieConsent() {
14
+ CookieConsent.show(true);
15
+ }
16
+
17
+ export function isAnalyticsAllowed(): boolean {
18
+ return CookieConsent.acceptedCategory("analytics");
19
+ }
20
+
21
+ const plausibleId = "plausible-script";
22
+
23
+ function syncPlausible(): void {
24
+ if (!settings.plausibleEnabled) {
25
+ disablePlausible();
26
+ return;
27
+ }
28
+
29
+ if (!isAnalyticsAllowed()) {
30
+ disablePlausible();
31
+ return;
32
+ }
33
+
34
+ enablePlausible();
35
+ }
36
+
37
+ function isPlausibleEnabled() {
38
+ return document.getElementById(plausibleId) !== null;
39
+ }
40
+
41
+ function disablePlausible() {
42
+ if (!isPlausibleEnabled()) {
43
+ return;
44
+ }
45
+
46
+ // The simplest and straightforward way to disable smth is to reload the page
47
+ // We expect that users will not reevaluate the cookie consent modal often
48
+ window.location.reload();
49
+ }
50
+
51
+ function enablePlausible() {
52
+ if (isPlausibleEnabled()) {
53
+ return;
54
+ }
55
+
56
+ console.log("setup Plausible script");
57
+
58
+ const script = document.createElement("script");
59
+ script.id = plausibleId;
60
+ script.src = settings.plausibleScript;
61
+ script.async = true;
62
+ script.defer = true;
63
+ script.setAttribute("data-domain", settings.plausibleDomain);
64
+ document.body.appendChild(script);
65
+ }
66
+
67
+ const _description = `
68
+ <p>We use cookies and local storage for session tracking (required) and optional analytics.</p>
69
+ <p>Please let us collect analytics to better understand how you use us and become the best news reader ever.</p>
70
+ <p>You can find more information in our <a href="/privacy" target="_blank" rel="noopener noreferrer">privacy policy</a>.</p>
71
+ `;
72
+
73
+ export const defaultConfig = {
74
+ revision: 1,
75
+
76
+ onConsent(): void {
77
+ syncPlausible();
78
+ },
79
+
80
+ onChange(): void {
81
+ syncPlausible();
82
+ },
83
+
84
+ categories: {
85
+ necessary: {
86
+ enabled: true,
87
+ readOnly: true
88
+ },
89
+ analytics: {
90
+ enabled: true,
91
+ readOnly: false
92
+ }
93
+ },
94
+
95
+ language: {
96
+ default: "en",
97
+ translations: {
98
+ en: {
99
+ consentModal: {
100
+ title: "We use cookies and local storage",
101
+ description: _description,
102
+ acceptAllBtn: "Accept all",
103
+ acceptNecessaryBtn: "Reject all",
104
+ showPreferencesBtn: "Manage preferences"
105
+ },
106
+ preferencesModal: {
107
+ title: "Manage privacy preferences",
108
+ acceptAllBtn: "Accept all",
109
+ acceptNecessaryBtn: "Reject all",
110
+ savePreferencesBtn: "Accept current selection",
111
+ closeIconLabel: "Close",
112
+ sections: [
113
+ {
114
+ description: _description
115
+ },
116
+ {
117
+ title: "Strictly necessary data",
118
+ description: "This data is essential for the proper functioning of the website and cannot be disabled.",
119
+
120
+ linkedCategory: "necessary",
121
+
122
+ cookieTable: {
123
+ headers: {
124
+ name: "Data",
125
+ description: "Description"
126
+ },
127
+ body: [
128
+ {
129
+ name: "Session information",
130
+ description:
131
+ "We store your session information which is absolutely necessary for the website to work."
132
+ }
133
+ ]
134
+ }
135
+ },
136
+ {
137
+ title: "Performance and analytics",
138
+ description: "These services collect information about how you use our website.",
139
+ linkedCategory: "analytics",
140
+
141
+ cookieTable: {
142
+ headers: {
143
+ name: "Service",
144
+ domain: "Domain",
145
+ description: "Description"
146
+ },
147
+ body: [
148
+ settings.plausibleEnabled
149
+ ? {
150
+ name: "Plausible",
151
+ domain: "plausible.io",
152
+ description:
153
+ "EU-based, cookie-free service that helps us measure traffic and improve usability, without collecting personal data."
154
+ }
155
+ : null,
156
+ {
157
+ name: "Feeds Fun",
158
+ domain: "feeds.fun",
159
+ description:
160
+ "We collect our own analytics to improve the service. We do not share this data with third parties."
161
+ }
162
+ ].filter((x) => x !== null)
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ }
168
+ }
169
+ }
170
+ };
@@ -8,7 +8,9 @@ import DiscoveryView from "../views/DiscoveryView.vue";
8
8
  import CollectionsView from "../views/CollectionsView.vue";
9
9
  import SettingsView from "../views/SettingsView.vue";
10
10
  import PublicCollectionView from "../views/PublicCollectionView.vue";
11
+ import CRMView from "../views/CRMView.vue";
11
12
  import * as e from "@/logic/enums";
13
+ import * as settings from "@/logic/settings";
12
14
 
13
15
  // lazy view loading does not work with router.push function
14
16
  // first attempt to router.push into not loaded view, will cause its loading, but will not change components
@@ -61,6 +63,18 @@ const router = createRouter({
61
63
  name: "public-collection",
62
64
  component: PublicCollectionView
63
65
  },
66
+ {
67
+ path: "/terms",
68
+ name: "terms",
69
+ component: CRMView,
70
+ props: {content: settings.crmTerms, kind: "terms"}
71
+ },
72
+ {
73
+ path: "/privacy",
74
+ name: "privacy",
75
+ component: CRMView,
76
+ props: {content: settings.crmPrivacy, kind: "privacy"}
77
+ },
64
78
  {
65
79
  path: "/:pathMatch(.*)*",
66
80
  redirect: "/"
@@ -10,7 +10,7 @@ export const useGlobalState = defineStore("globalState", () => {
10
10
  const supertokens = useSupertokens();
11
11
 
12
12
  const isLoggedIn = computed(() => {
13
- if (settings.authMode === settings.AuthMode.SingleUser) {
13
+ if (settings.isSingleUserMode) {
14
14
  return true;
15
15
  }
16
16
 
package/src/style.css CHANGED
@@ -1,5 +1,3 @@
1
- @import url("https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css");
2
-
3
1
  @import "tailwindcss/base";
4
2
  @import "tailwindcss/components";
5
3
  @import "tailwindcss/utilities";
@@ -2,9 +2,12 @@
2
2
  <a
3
3
  :href="url"
4
4
  target="_blank"
5
+ class="whitespace-nowrap"
5
6
  rel="noopener noreferrer">
6
7
  {{ renderedText }}
7
- <i class="ti ti-external-link" />
8
+ <icon
9
+ icon="external-link"
10
+ size="small" />
8
11
  </a>
9
12
  </template>
10
13
 
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <component
3
+ :is="componentName"
4
+ class="inline-block"
5
+ v-bind="iconProperties" />
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import {computed} from "vue";
10
+ import {
11
+ IconArrowNarrowRight,
12
+ IconArrowRight,
13
+ IconPlus,
14
+ IconDots,
15
+ IconBrandReddit,
16
+ IconBrandDiscord,
17
+ IconBrandGithub,
18
+ IconExternalLink,
19
+ IconChevronsLeft,
20
+ IconChevronsRight,
21
+ IconLayoutSidebarLeftCollapse,
22
+ IconLayoutSidebarLeftExpand,
23
+ IconX,
24
+ IconMoodSmile,
25
+ IconMoodSad
26
+ } from "@tabler/icons-vue";
27
+
28
+ const iconMap = {
29
+ reddit: IconBrandReddit,
30
+ discord: IconBrandDiscord,
31
+ github: IconBrandGithub,
32
+ "arrow-narrow-right": IconArrowNarrowRight,
33
+ "arrow-right": IconArrowRight,
34
+ plus: IconPlus,
35
+ dots: IconDots,
36
+ "external-link": IconExternalLink,
37
+ "chevrons-right": IconChevronsRight,
38
+ "chevrons-left": IconChevronsLeft,
39
+ "sidebar-left-collapse": IconLayoutSidebarLeftCollapse,
40
+ "sidebar-left-expand": IconLayoutSidebarLeftExpand,
41
+ x: IconX,
42
+ "face-smile": IconMoodSmile,
43
+ "face-sad": IconMoodSad
44
+ } as const;
45
+
46
+ type IconName = keyof typeof iconMap;
47
+
48
+ const sizeMap = {
49
+ small: 16,
50
+ medium: 20,
51
+ large: 24
52
+ } as const;
53
+
54
+ type IconSize = keyof typeof sizeMap;
55
+
56
+ const properties = defineProps<{icon: IconName; size?: IconSize}>();
57
+
58
+ const componentName = computed(() => {
59
+ if (properties.icon in iconMap) {
60
+ return iconMap[properties.icon];
61
+ } else {
62
+ throw new Error(`Icon ${properties.icon} not found`);
63
+ }
64
+ });
65
+
66
+ const iconProperties = computed(() => {
67
+ return {
68
+ size: sizeMap[properties.size ?? "medium"]
69
+ };
70
+ });
71
+ </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <a
3
+ v-if="link.enabled"
4
+ :href="link.url"
5
+ target="_blank"
6
+ @click="events.socialLinkClicked({linkType: link.eventType, view: eventsView})">
7
+ <span v-if="mode === 'text'">
8
+ {{ link.text }}
9
+
10
+ <icon
11
+ icon="external-link"
12
+ size="small" />
13
+ </span>
14
+
15
+ <icon
16
+ v-else
17
+ :icon="link.icon" />
18
+ </a>
19
+ </template>
20
+
21
+ <script lang="ts" setup>
22
+ import {inject, computed} from "vue";
23
+
24
+ import * as events from "@/logic/events";
25
+ import * as settings from "@/logic/settings";
26
+ import * as asserts from "@/logic/asserts";
27
+
28
+ const links = {
29
+ api: {
30
+ enabled: true,
31
+ url: "/api/docs",
32
+ text: "API",
33
+ icon: null,
34
+ eventType: "api"
35
+ },
36
+ blog: {
37
+ enabled: settings.blog !== null,
38
+ url: settings.blog,
39
+ text: "Blog",
40
+ icon: null,
41
+ eventType: "blog"
42
+ },
43
+ reddit: {
44
+ enabled: settings.redditSubreddit !== null,
45
+ url: settings.redditSubreddit,
46
+ text: "Reddit",
47
+ icon: "reddit",
48
+ eventType: "reddit"
49
+ },
50
+ discord: {
51
+ enabled: settings.discordInvite !== null,
52
+ url: settings.discordInvite,
53
+ text: "Discord",
54
+ icon: "discord",
55
+ eventType: "discord"
56
+ },
57
+ github: {
58
+ enabled: settings.githubRepo !== null,
59
+ url: settings.githubRepo,
60
+ text: "GitHub",
61
+ icon: "github",
62
+ eventType: "github"
63
+ }
64
+ };
65
+
66
+ type LinkKind = keyof typeof links;
67
+
68
+ const properties = defineProps<{kind: LinkKind; mode: "icon" | "text"}>();
69
+
70
+ const eventsView = inject<events.EventsViewName>("eventsViewName");
71
+
72
+ asserts.defined(eventsView);
73
+
74
+ const link = computed(() => {
75
+ if (properties.kind in links) {
76
+ return links[properties.kind];
77
+ }
78
+
79
+ throw new Error(`Link kind "${properties.kind}" not found`);
80
+ });
81
+ </script>
@@ -1,12 +1,23 @@
1
1
  <template>
2
2
  <wide-layout>
3
- <template #header> Feeds Fun </template>
3
+ <div class="ffun-page-header">
4
+ <div class="ffun-page-header-center-block">
5
+ <page-header-external-links :show-api="false" />
6
+ </div>
7
+ </div>
4
8
 
5
- <div class="ffun-info-good">
6
- <p v-if="!linkProcessed">Checking login status...</p>
9
+ <hr />
7
10
 
8
- <supertokens-login v-else />
9
- </div>
11
+ <main-block>
12
+ <h1 class="m-0 text-5xl">Feeds Fun</h1>
13
+ <p class="mt-2 text-2xl">Transparent Personalized News</p>
14
+ </main-block>
15
+
16
+ <main-block>
17
+ <div class="max-w-xl md:mx-auto ffun-info-good text-center mx-2">
18
+ <p>Checking login status...</p>
19
+ </div>
20
+ </main-block>
10
21
  </wide-layout>
11
22
  </template>
12
23
 
@@ -28,12 +39,14 @@
28
39
 
29
40
  provide("eventsViewName", "auth");
30
41
 
31
- const linkProcessed = ref(false);
32
-
33
42
  function goToWorkspace() {
34
43
  router.push({name: globalSettings.mainPanelMode, params: {}});
35
44
  }
36
45
 
46
+ function goToMain() {
47
+ router.push({name: "main", params: {}});
48
+ }
49
+
37
50
  onBeforeMount(async () => {
38
51
  if (globalState.isLoggedIn) {
39
52
  goToWorkspace();
@@ -46,6 +59,7 @@
46
59
 
47
60
  async function onSignFailed() {
48
61
  await supertokens.clearLoginAttempt();
62
+ goToMain();
49
63
  }
50
64
 
51
65
  if (await supertokens.hasInitialMagicLinkBeenSent()) {
@@ -54,9 +68,8 @@
54
68
  onSignIn: onSignIn,
55
69
  onSignFailed: onSignFailed
56
70
  });
57
- linkProcessed.value = true;
58
71
  } else {
59
- linkProcessed.value = true;
72
+ goToMain();
60
73
  }
61
74
  });
62
75
  </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <wide-layout>
3
+ <div class="ffun-page-header">
4
+ <div class="ffun-page-header-center-block">
5
+ <page-header-home-button />
6
+ <page-header-external-links :show-api="false" />
7
+ </div>
8
+ </div>
9
+
10
+ <hr />
11
+
12
+ <main-block>
13
+ <h1 class="m-0 text-5xl">Feeds Fun</h1>
14
+ <p class="mt-2 text-2xl">Transparent Personalized News</p>
15
+ </main-block>
16
+
17
+ <hr />
18
+
19
+ <main-block>
20
+ <div
21
+ v-html="actualContent"
22
+ class="prose max-w-none" />
23
+ </main-block>
24
+ </wide-layout>
25
+ </template>
26
+
27
+ <script lang="ts" setup>
28
+ import {computed, provide} from "vue";
29
+
30
+ const properties = defineProps<{content: string | null; kind: string}>();
31
+
32
+ provide("eventsViewName", properties.kind);
33
+
34
+ const actualContent = computed(() => {
35
+ if (properties.content === null) {
36
+ return "You MUST define a content for this page in case you host public version of Feeds Fun.";
37
+ }
38
+
39
+ return properties.content;
40
+ });
41
+ </script>
@@ -28,7 +28,7 @@
28
28
  <main-header-line> Smarter way to read news </main-header-line>
29
29
 
30
30
  <main-block>
31
- <main-description icon="ti-number-1">
31
+ <main-description step="1">
32
32
  <template #caption> Subscribe to sites </template>
33
33
 
34
34
  <template #description>
@@ -36,7 +36,7 @@
36
36
  </template>
37
37
  </main-description>
38
38
 
39
- <main-description icon="ti-number-2">
39
+ <main-description step="2">
40
40
  <template #caption> Get automatic tagging </template>
41
41
 
42
42
  <template #description>
@@ -73,7 +73,7 @@
73
73
  </template>
74
74
  </main-description>
75
75
 
76
- <main-description icon="ti-number-3">
76
+ <main-description step="3">
77
77
  <template #caption> Create scoring rules </template>
78
78
 
79
79
  <template #description>
@@ -85,9 +85,12 @@
85
85
  :link="null"
86
86
  css-modifier="positive" />
87
87
 
88
- <i class="ti ti-arrow-right"></i>
88
+ <icon
89
+ icon="arrow-right"
90
+ size="small"
91
+ class="mx-0.5" />
89
92
 
90
- <span class="cursor-default text-purple-700 text-lg md:text-xl">+5</span>
93
+ <span class="inline-block align-middle cursor-default text-purple-700 text-lg md:text-xl">+5</span>
91
94
  </div>
92
95
 
93
96
  <div class="">
@@ -97,9 +100,12 @@
97
100
  link="http://example.com"
98
101
  css-modifier="negative" />
99
102
 
100
- <i class="ti ti-arrow-right"></i>
103
+ <icon
104
+ icon="arrow-right"
105
+ size="small"
106
+ class="mx-0.5" />
101
107
 
102
- <span class="cursor-default text-purple-700 text-lg md:text-xl">-55</span>
108
+ <span class="inline-block align-middle cursor-default text-purple-700 text-lg md:text-xl">-55</span>
103
109
  </div>
104
110
 
105
111
  <div class="">
@@ -109,7 +115,10 @@
109
115
  :link="null"
110
116
  css-modifier="positive" />
111
117
 
112
- <i class="ti ti-plus"></i>
118
+ <icon
119
+ icon="plus"
120
+ size="small"
121
+ class="mx-0.5" />
113
122
 
114
123
  <fake-tag
115
124
  uid="new-york"
@@ -117,9 +126,12 @@
117
126
  :link="null"
118
127
  css-modifier="positive" />
119
128
 
120
- <i class="ti ti-arrow-right"></i>
129
+ <icon
130
+ icon="arrow-right"
131
+ size="small"
132
+ class="mx-0.5" />
121
133
 
122
- <span class="cursor-default-purple-700 text-lg md:text-xl">+8</span>
134
+ <span class="inline-block align-middle cursor-default text-purple-700 text-lg md:text-xl">+8</span>
123
135
  </div>
124
136
 
125
137
  <div class="">
@@ -129,15 +141,18 @@
129
141
  :link="null"
130
142
  css-modifier="positive" />
131
143
 
132
- <i class="ti ti-arrow-right"></i>
144
+ <icon
145
+ icon="arrow-right"
146
+ size="small"
147
+ class="mx-0.5" />
133
148
 
134
- <span class="cursor-default text-purple-700 text-lg md:text-xl">+21</span>
149
+ <span class="inline-block align-middle cursor-default text-purple-700 text-lg md:text-xl">+21</span>
135
150
  </div>
136
151
  </div>
137
152
  </template>
138
153
  </main-description>
139
154
 
140
- <main-description icon="ti-number-4">
155
+ <main-description step="4">
141
156
  <template #caption> Read what matters </template>
142
157
 
143
158
  <template #description>
@@ -151,7 +166,9 @@
151
166
  title="Sci-fi novel about UFO in New Yourk"
152
167
  :score="13" />
153
168
 
154
- <i class="opacity-65 block ti ti-dots justify-self-center"></i>
169
+ <div class="opacity-65 block justify-self-center">
170
+ <icon icon="dots" />
171
+ </div>
155
172
 
156
173
  <main-news-title
157
174
  class="opacity-55"
@@ -171,9 +188,7 @@
171
188
  <template
172
189
  v-for="collectionId in collections.collectionsOrder"
173
190
  :key="collectionId">
174
- <main-item
175
- v-if="collections.collections[collectionId].showOnMain"
176
- icon="ti-arrow-narrow-right">
191
+ <main-item v-if="collections.collections[collectionId].showOnMain">
177
192
  <template #caption>
178
193
  {{ collections.collections[collectionId].name }}
179
194
  </template>
@@ -116,6 +116,32 @@
116
116
  </tr>
117
117
  </tbody>
118
118
  </table>
119
+
120
+ <h3>Danger Zone</h3>
121
+
122
+ <div class="ffun-info-bad">
123
+ <p><strong>ATTENTION!</strong></p>
124
+
125
+ <p> Operations in this section are irreversible and may lead to data loss and even account deletion. </p>
126
+ </div>
127
+
128
+ <div
129
+ v-if="!settings.isSingleUserMode"
130
+ class="ffun-info-bad">
131
+ <button
132
+ @click.prevent="removeAccount()"
133
+ class="ffun-form-button bad short ml-1"
134
+ >Remove Account</button
135
+ >
136
+
137
+ <label class="ml-1"> Permanently remove your account and all your data. </label>
138
+ </div>
139
+
140
+ <div
141
+ v-else
142
+ class="ffun-info-common">
143
+ <p> Account removal in the single-user mode is not available. </p>
144
+ </div>
119
145
  </side-panel-layout>
120
146
  </template>
121
147
 
@@ -125,6 +151,7 @@
125
151
  import * as api from "@/logic/api";
126
152
  import * as t from "@/logic/types";
127
153
  import * as e from "@/logic/enums";
154
+ import * as settings from "@/logic/settings";
128
155
  import {useRouter} from "vue-router";
129
156
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
130
157
 
@@ -158,6 +185,12 @@
158
185
  router.push({name: e.MainPanelMode.Collections, params: {}});
159
186
  }
160
187
 
188
+ function removeAccount() {
189
+ if (confirm("Are you sure you want to remove your account? THIS OPERATION IS NOT REVERSIBLE!")) {
190
+ api.removeUser();
191
+ }
192
+ }
193
+
161
194
  // TODO: check api keys on setup
162
195
  // TODO: basic integer checks
163
196
  </script>