From f2b39b046c19b08b3e8f2045ed67ae6778d1517f Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:04:46 -0700 Subject: [PATCH] Add support bar and donation modal to homepage Introduces a support bar component on the homepage to display project funding progress and encourage donations. Adds a modal with more information about supporting the project. Updates configuration to allow toggling the support bar and setting funding values. Updates links to the new donation page and adds related translations. --- public/notifications.xml | 2 +- src/assets/locales/en.json | 12 ++ src/components/LinksDropdown.tsx | 2 +- src/components/layout/Footer.tsx | 2 +- src/components/overlays/SupportInfoModal.tsx | 40 ++++++ src/pages/HomePage.tsx | 3 + src/pages/parts/home/SupportBar.tsx | 131 +++++++++++++++++++ src/setup/App.tsx | 2 + src/setup/config.ts | 8 ++ 9 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 src/components/overlays/SupportInfoModal.tsx create mode 100644 src/pages/parts/home/SupportBar.tsx diff --git a/public/notifications.xml b/public/notifications.xml index 8ab9835b..28c9f676 100644 --- a/public/notifications.xml +++ b/public/notifications.xml @@ -257,7 +257,7 @@ It will improve uptime for FED API and faster EU streams, another proxy which we If you are interested in donating, please check the link below! - https://rentry.co/h5mypdfs + https://rentry.co/nnqtas3e Sat, 06 Sep 2025 14:42:00 MST announcement diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index af290b9a..36de4fc0 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -392,6 +392,18 @@ "It's the Great Pumpkin, Charlie Brown!" ] } + }, + "support": { + "title": "P-Stream needs your help!", + "description": "P-Stream is run at a loss, and we need help to keep it ad free! If you enjoy using P-Stream, please consider donating to help us cover our costs.", + "moreInfo": "More info", + "explanation": "If you aren't using the extension or don't have FED API set up, it may be harder to find content! We want to fix this, but it's a lot harder to provide content without expensive servers. So please, if you enjoy using P-Stream, please consider donating to help us cover our growing costs.", + "explanation2": "If you want more info, please join our ", + "discord": "Discord", + "thankYou": "Thank you for your support!", + "donate": "Donate", + "label": "Project Funding: ${{current}} / ${{goal}}", + "complete": "complete" } }, "media": { diff --git a/src/components/LinksDropdown.tsx b/src/components/LinksDropdown.tsx index d4338b4b..50ae38c0 100644 --- a/src/components/LinksDropdown.tsx +++ b/src/components/LinksDropdown.tsx @@ -315,7 +315,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) { /> diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 458f2294..5eb71c42 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -83,7 +83,7 @@ export function Footer() { {t("footer.links.discord")} - + {t("footer.links.funding")}
diff --git a/src/components/overlays/SupportInfoModal.tsx b/src/components/overlays/SupportInfoModal.tsx new file mode 100644 index 00000000..573af03c --- /dev/null +++ b/src/components/overlays/SupportInfoModal.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from "react-i18next"; + +import { FancyModal } from "./Modal"; +import { Button } from "../buttons/Button"; +import { MwLink } from "../text/Link"; + +export function SupportInfoModal({ id }: { id: string }) { + const { t } = useTranslation(); + + return ( + +
+

{t("home.support.explanation")}

+

+ {t("home.support.explanation2")}{" "} + + {t("home.support.discord")} + +

+ +
+ + + +
+ +
+ {t("home.support.thankYou")} +
+
+
+ ); +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 88a97ee0..ddd0bf92 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -25,6 +25,7 @@ import { MediaItem } from "@/utils/mediaTypes"; import { Button } from "./About"; import { AdsPart } from "./parts/home/AdsPart"; +import { SupportBar } from "./parts/home/SupportBar"; function useSearch(search: string) { const [searching, setSearching] = useState(false); @@ -171,6 +172,8 @@ export function HomePage() { /> )} + {conf().SHOW_SUPPORT_BAR ? : null} + {conf().SHOW_AD ? : null}
diff --git a/src/pages/parts/home/SupportBar.tsx b/src/pages/parts/home/SupportBar.tsx new file mode 100644 index 00000000..7f170826 --- /dev/null +++ b/src/pages/parts/home/SupportBar.tsx @@ -0,0 +1,131 @@ +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Icon, Icons } from "@/components/Icon"; +import { SettingsCard } from "@/components/layout/SettingsCard"; +import { MwLink } from "@/components/text/Link"; +import { Heading3 } from "@/components/utils/Text"; +import { conf } from "@/setup/config"; +import { useOverlayStack } from "@/stores/interface/overlayStack"; + +function getCookie(name: string): string | null { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i += 1) { + const cookie = cookies[i].trim(); + if (cookie.startsWith(`${name}=`)) { + return cookie.substring(name.length + 1); + } + } + return null; +} + +function setCookie(name: string, value: string, expiryDays: number): void { + const date = new Date(); + date.setTime(date.getTime() + expiryDays * 24 * 60 * 60 * 1000); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${value};${expires};path=/`; +} + +export function SupportBar() { + const { t } = useTranslation(); + const { showModal } = useOverlayStack(); + const [isDescriptionDismissed, setIsDescriptionDismissed] = useState(() => { + return getCookie("supportDescriptionDismissed") === "true"; + }); + + const toggleDescription = useCallback(() => { + const newState = !isDescriptionDismissed; + setIsDescriptionDismissed(newState); + setCookie("supportDescriptionDismissed", newState ? "true" : "false", 14); // Expires after 14 days + }, [isDescriptionDismissed]); + + const openSupportModal = useCallback(() => { + showModal("support-info"); + }, [showModal]); + + const supportValue = conf().SUPPORT_BAR_VALUE; + if (!supportValue) return null; + + // Parse fraction like "100/300" + const [currentStr, goalStr] = supportValue.split("/"); + const current = parseFloat(currentStr) || 0; + const goal = parseFloat(goalStr) || 1; + + const percentage = Math.min((current / goal) * 100, 100); + + return ( +
+
+ + +
+ + {t("home.support.title")} + +

+ {t("home.support.description")} +

+
+
+ + {t("home.support.label", { + current: current.toLocaleString(), + goal: goal.toLocaleString(), + })} + + + {percentage.toFixed(1)}% {t("home.support.complete")} + +
+
+
+ {/* Progress bar */} +
+
+
+
+ + + + + + {t("home.support.donate")} + + +
+ +
+
+ ); +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index ad08441c..ff6bf327 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -14,6 +14,7 @@ import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb"; import { DetailsModal } from "@/components/overlays/detailsModal"; import { KeyboardCommandsModal } from "@/components/overlays/KeyboardCommandsModal"; import { NotificationModal } from "@/components/overlays/notificationsModal"; +import { SupportInfoModal } from "@/components/overlays/SupportInfoModal"; import { useGlobalKeyboardEvents } from "@/hooks/useGlobalKeyboardEvents"; import { useOnlineListener } from "@/hooks/usePing"; import { AboutPage } from "@/pages/About"; @@ -126,6 +127,7 @@ function App() { + diff --git a/src/setup/config.ts b/src/setup/config.ts index d5ac0c81..2f862009 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -34,6 +34,8 @@ interface Config { BANNER_ID: string; USE_TRAKT: boolean; HIDE_PROXY_ONBOARDING: boolean; + SHOW_SUPPORT_BAR: boolean; + SUPPORT_BAR_VALUE: string; } export interface RuntimeConfig { @@ -64,6 +66,8 @@ export interface RuntimeConfig { BANNER_ID: string | null; USE_TRAKT: boolean; HIDE_PROXY_ONBOARDING: boolean; + SHOW_SUPPORT_BAR: boolean; + SUPPORT_BAR_VALUE: string; } const env: Record = { @@ -97,6 +101,8 @@ const env: Record = { BANNER_ID: import.meta.env.VITE_BANNER_ID, USE_TRAKT: import.meta.env.VITE_USE_TRAKT, HIDE_PROXY_ONBOARDING: import.meta.env.VITE_HIDE_PROXY_ONBOARDING, + SHOW_SUPPORT_BAR: import.meta.env.VITE_SHOW_SUPPORT_BAR, + SUPPORT_BAR_VALUE: import.meta.env.VITE_SUPPORT_BAR_VALUE, }; function coerceUndefined(value: string | null | undefined): string | undefined { @@ -173,5 +179,7 @@ export function conf(): RuntimeConfig { BANNER_ID: getKey("BANNER_ID"), USE_TRAKT: getKey("USE_TRAKT", "false") === "true", HIDE_PROXY_ONBOARDING: getKey("HIDE_PROXY_ONBOARDING", "false") === "true", + SHOW_SUPPORT_BAR: getKey("SHOW_SUPPORT_BAR", "false") === "true", + SUPPORT_BAR_VALUE: getKey("SUPPORT_BAR_VALUE") ?? "", }; }