mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
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.
This commit is contained in:
parent
dbf2c02a53
commit
f2b39b046c
9 changed files with 199 additions and 3 deletions
|
|
@ -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!
|
||||
</description>
|
||||
<link>https://rentry.co/h5mypdfs</link>
|
||||
<link>https://rentry.co/nnqtas3e</link>
|
||||
<pubDate>Sat, 06 Sep 2025 14:42:00 MST</pubDate>
|
||||
<category>announcement</category>
|
||||
</item>
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||
/>
|
||||
<CircleDropdownLink href="/support" icon={Icons.SUPPORT} />
|
||||
<CircleDropdownLink
|
||||
href="https://rentry.co/h5mypdfs"
|
||||
href="https://rentry.co/nnqtas3e"
|
||||
icon={Icons.TIP_JAR}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function Footer() {
|
|||
<FooterLink icon={Icons.DISCORD} href={conf().DISCORD_LINK}>
|
||||
{t("footer.links.discord")}
|
||||
</FooterLink>
|
||||
<FooterLink href="https://rentry.co/h5mypdfs" icon={Icons.TIP_JAR}>
|
||||
<FooterLink href="https://rentry.co/nnqtas3e" icon={Icons.TIP_JAR}>
|
||||
{t("footer.links.funding")}
|
||||
</FooterLink>
|
||||
<div className="inline md:hidden">
|
||||
|
|
|
|||
40
src/components/overlays/SupportInfoModal.tsx
Normal file
40
src/components/overlays/SupportInfoModal.tsx
Normal file
|
|
@ -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 (
|
||||
<FancyModal id={id} title={t("home.support.title")} size="md">
|
||||
<div className="space-y-4">
|
||||
<p className="text-type-secondary">{t("home.support.explanation")}</p>
|
||||
<p className="text-type-secondary">
|
||||
{t("home.support.explanation2")}{" "}
|
||||
<MwLink url="https://discord.gg/7z6znYgrTG">
|
||||
{t("home.support.discord")}
|
||||
</MwLink>
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<span className="text-center flex justify-center whitespace-nowrap items-center">
|
||||
<Button
|
||||
theme="purple"
|
||||
onClick={() =>
|
||||
window.open("https://rentry.co/nnqtas3e", "_blank")
|
||||
}
|
||||
>
|
||||
{t("home.support.donate")}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-type-dimmed text-center">
|
||||
{t("home.support.thankYou")}
|
||||
</div>
|
||||
</div>
|
||||
</FancyModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<boolean>(false);
|
||||
|
|
@ -171,6 +172,8 @@ export function HomePage() {
|
|||
/>
|
||||
)}
|
||||
|
||||
{conf().SHOW_SUPPORT_BAR ? <SupportBar /> : null}
|
||||
|
||||
{conf().SHOW_AD ? <AdsPart /> : null}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
131
src/pages/parts/home/SupportBar.tsx
Normal file
131
src/pages/parts/home/SupportBar.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="w-full px-4 py-2">
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<SettingsCard className="max-w-md relative group">
|
||||
<button
|
||||
onClick={toggleDescription}
|
||||
type="button"
|
||||
className="absolute z-20 -top-2 -right-2 w-6 h-6 bg-mediaCard-hoverBackground rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
aria-label={
|
||||
isDescriptionDismissed ? "Show description" : "Hide description"
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
className="text-s font-semibold text-type-secondary"
|
||||
icon={
|
||||
isDescriptionDismissed ? Icons.CHEVRON_UP : Icons.CHEVRON_DOWN
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ${
|
||||
isDescriptionDismissed
|
||||
? "max-h-0 opacity-0 pb-0"
|
||||
: "max-h-32 opacity-100 pb-4"
|
||||
}`}
|
||||
>
|
||||
<Heading3 className="transition-opacity duration-300">
|
||||
{t("home.support.title")}
|
||||
</Heading3>
|
||||
<p className="text-type-secondary max-w-md pb-4 transition-opacity duration-300">
|
||||
{t("home.support.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center text-sm text-type-dimmed w-full max-w-md pb-4">
|
||||
<span className="text-left">
|
||||
{t("home.support.label", {
|
||||
current: current.toLocaleString(),
|
||||
goal: goal.toLocaleString(),
|
||||
})}
|
||||
</span>
|
||||
<span className="ml-auto text-right flex-shrink-0 whitespace-nowrap">
|
||||
{percentage.toFixed(1)}% {t("home.support.complete")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full max-w-md">
|
||||
<div className="relative w-full h-2 bg-progress-background bg-opacity-25 rounded-full">
|
||||
{/* Progress bar */}
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full rounded-full bg-progress-filled transition-all duration-300"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center text-sm text-type-dimmed w-full max-w-md pt-4">
|
||||
<span className="text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openSupportModal}
|
||||
className="group mt-1 cursor-pointer font-bold text-type-link hover:text-type-linkHover active:scale-95"
|
||||
>
|
||||
{t("home.support.moreInfo")}
|
||||
</button>
|
||||
</span>
|
||||
<span className="ml-auto text-right flex-shrink-0 whitespace-nowrap">
|
||||
<MwLink url="https://rentry.co/nnqtas3e">
|
||||
{t("home.support.donate")}
|
||||
</MwLink>
|
||||
</span>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
<LanguageProvider />
|
||||
<NotificationModal id="notifications" />
|
||||
<KeyboardCommandsModal id="keyboard-commands" />
|
||||
<SupportInfoModal id="support-info" />
|
||||
<DetailsModal id="details" />
|
||||
<DetailsModal id="discover-details" />
|
||||
<DetailsModal id="player-details" />
|
||||
|
|
|
|||
|
|
@ -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<keyof Config, undefined | string> = {
|
||||
|
|
@ -97,6 +101,8 @@ const env: Record<keyof Config, undefined | string> = {
|
|||
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") ?? "",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue