1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

Try adding vercel analytics

This commit is contained in:
Cooper Ransom 2024-02-26 18:03:35 -05:00
parent e0b59c011b
commit 677b53b172
11 changed files with 438 additions and 0 deletions

View file

@ -36,6 +36,7 @@
"@scure/bip39": "^1.2.2",
"@sozialhelden/ietf-language-tags": "^5.4.2",
"@types/node-forge": "^1.3.10",
"@vercel/analytics": "^1.2.2",
"classnames": "^2.3.2",
"core-js": "^3.34.0",
"detect-browser": "^5.3.0",

View file

@ -42,6 +42,9 @@ dependencies:
'@types/node-forge':
specifier: ^1.3.10
version: 1.3.10
'@vercel/analytics':
specifier: ^1.2.2
version: 1.2.2(react@18.2.0)
classnames:
specifier: ^2.3.2
version: 2.3.2
@ -2488,6 +2491,21 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@vercel/analytics@1.2.2(react@18.2.0):
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
peerDependencies:
next: '>= 13'
react: ^18 || ^19
peerDependenciesMeta:
next:
optional: true
react:
optional: true
dependencies:
react: 18.2.0
server-only: 0.0.1
dev: false
/@vitejs/plugin-react@4.2.1(vite@5.0.12):
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -6172,6 +6190,10 @@ packages:
randombytes: 2.1.0
dev: true
/server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
dev: false
/set-cookie-parser@2.6.0:
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
dev: false

View file

@ -1,3 +1,4 @@
import { Analytics } from "@vercel/analytics/react";
import { ReactElement, Suspense, lazy, useEffect } from "react";
import { lazyWithPreload } from "react-lazy-with-preload";
import {
@ -155,6 +156,7 @@ function App() {
) : null}
<Route path="*" element={<NotFoundPage />} />
</Routes>
<Analytics />
</Layout>
);
}

162
src/utils/setup/App.tsx Normal file
View file

@ -0,0 +1,162 @@
import { ReactElement, Suspense, lazy, useEffect } from "react";
import { lazyWithPreload } from "react-lazy-with-preload";
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
import { useOnlineListener } from "@/hooks/usePing";
import { AboutPage } from "@/pages/About";
import { AdminPage } from "@/pages/admin/AdminPage";
import VideoTesterView from "@/pages/developer/VideoTesterView";
import { DmcaPage, shouldHaveDmcaPage } from "@/pages/Dmca";
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
import { HomePage } from "@/pages/HomePage";
import { LoginPage } from "@/pages/Login";
import { OnboardingPage } from "@/pages/onboarding/Onboarding";
import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension";
import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy";
import { RegisterPage } from "@/pages/Register";
import { Layout } from "@/setup/Layout";
import { useHistoryListener } from "@/stores/history";
import { LanguageProvider } from "@/stores/language";
const DeveloperPage = lazy(() => import("@/pages/DeveloperPage"));
const TestView = lazy(() => import("@/pages/developer/TestView"));
const PlayerView = lazyWithPreload(() => import("@/pages/PlayerView"));
const SettingsPage = lazyWithPreload(() => import("@/pages/Settings"));
PlayerView.preload();
SettingsPage.preload();
function LegacyUrlView({ children }: { children: ReactElement }) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
const url = location.pathname;
if (!isLegacyUrl(url)) return;
convertLegacyUrl(location.pathname).then((convertedUrl) => {
navigate(convertedUrl ?? "/", { replace: true });
});
}, [location.pathname, navigate]);
if (isLegacyUrl(location.pathname)) return null;
return children;
}
function QuickSearch() {
const { query } = useParams<{ query: string }>();
const navigate = useNavigate();
useEffect(() => {
if (query) {
generateQuickSearchMediaUrl(query).then((url) => {
navigate(url ?? "/", { replace: true });
});
} else {
navigate("/", { replace: true });
}
}, [query, navigate]);
return null;
}
function QueryView() {
const { query } = useParams<{ query: string }>();
const navigate = useNavigate();
useEffect(() => {
if (query) {
navigate(`/browse/${query}`, { replace: true });
} else {
navigate("/", { replace: true });
}
}, [query, navigate]);
return null;
}
function App() {
useHistoryListener();
useOnlineListener();
return (
<Layout>
<LanguageProvider />
<Routes>
{/* functional routes */}
<Route path="/s/:query" element={<QuickSearch />} />
<Route path="/search/:type" element={<Navigate to="/browse" />} />
<Route path="/search/:type/:query?" element={<QueryView />} />
{/* pages */}
<Route
path="/media/:media"
element={
<LegacyUrlView>
<Suspense fallback={null}>
<PlayerView />
</Suspense>
</LegacyUrlView>
}
/>
<Route
path="/media/:media/:season/:episode"
element={
<LegacyUrlView>
<Suspense fallback={null}>
<PlayerView />
</Suspense>
</LegacyUrlView>
}
/>
<Route path="/browse/:query?" element={<HomePage />} />
<Route path="/" element={<HomePage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
<Route
path="/onboarding/extension"
element={<OnboardingExtensionPage />}
/>
<Route path="/onboarding/proxy" element={<OnboardingProxyPage />} />
{shouldHaveDmcaPage() ? (
<Route path="/dmca" element={<DmcaPage />} />
) : null}
{/* Settings page */}
<Route
path="/settings"
element={
<Suspense fallback={null}>
<SettingsPage />
</Suspense>
}
/>
{/* admin routes */}
<Route path="/admin" element={<AdminPage />} />
{/* other */}
<Route path="/dev" element={<DeveloperPage />} />
<Route path="/dev/video" element={<VideoTesterView />} />
{/* developer routes that can abuse workers are disabled in production */}
{process.env.NODE_ENV === "development" ? (
<Route path="/dev/test" element={<TestView />} />
) : null}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Layout>
);
}
export default App;

View file

@ -0,0 +1,25 @@
import { ReactNode } from "react";
import { useBannerSize, useBannerStore } from "@/stores/banner";
import { BannerLocation } from "@/stores/banner/BannerLocation";
export function Layout(props: { children: ReactNode }) {
const bannerSize = useBannerSize();
const location = useBannerStore((s) => s.location);
return (
<div>
<div className="fixed inset-x-0 z-[1000]">
<BannerLocation />
</div>
<div
style={{
paddingTop: location === null ? `${bannerSize}px` : "0px",
}}
className="flex min-h-screen flex-col"
>
{props.children}
</div>
</div>
);
}

View file

@ -0,0 +1,30 @@
const CHROMECAST_SENDER_SDK =
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
const callbacks: ((available: boolean) => void)[] = [];
let _available: boolean | null = null;
function init(available: boolean) {
_available = available;
callbacks.forEach((cb) => cb(available));
}
export function isChromecastAvailable(cb: (available: boolean) => void) {
if (_available !== null) return cb(_available);
callbacks.push(cb);
}
export function initializeChromecast() {
window.__onGCastApiAvailable = (isAvailable) => {
init(isAvailable);
};
// add script if doesnt exist yet
const exists = !!document.getElementById("chromecast-script");
if (!exists) {
const script = document.createElement("script");
script.setAttribute("src", CHROMECAST_SENDER_SDK);
script.setAttribute("id", "chromecast-script");
document.body.appendChild(script);
}
}

125
src/utils/setup/config.ts Normal file
View file

@ -0,0 +1,125 @@
import {
APP_VERSION,
BACKEND_URL,
DISCORD_LINK,
DONATION_LINK,
GITHUB_LINK,
} from "./constants";
interface Config {
APP_VERSION: string;
GITHUB_LINK: string;
DONATION_LINK: string;
DISCORD_LINK: string;
DMCA_EMAIL: string;
TMDB_READ_API_KEY: string;
CORS_PROXY_URL: string;
NORMAL_ROUTER: boolean;
BACKEND_URL: string;
DISALLOWED_IDS: string;
TURNSTILE_KEY: string;
CDN_REPLACEMENTS: string;
HAS_ONBOARDING: string;
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string;
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string;
ONBOARDING_PROXY_INSTALL_LINK: string;
}
export interface RuntimeConfig {
APP_VERSION: string;
GITHUB_LINK: string;
DONATION_LINK: string;
DISCORD_LINK: string;
DMCA_EMAIL: string | null;
TMDB_READ_API_KEY: string;
NORMAL_ROUTER: boolean;
PROXY_URLS: string[];
BACKEND_URL: string;
DISALLOWED_IDS: string[];
TURNSTILE_KEY: string | null;
CDN_REPLACEMENTS: Array<string[]>;
HAS_ONBOARDING: boolean;
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_PROXY_INSTALL_LINK: string | null;
}
const env: Record<keyof Config, undefined | string> = {
TMDB_READ_API_KEY: import.meta.env.VITE_TMDB_READ_API_KEY,
APP_VERSION: undefined,
GITHUB_LINK: undefined,
DONATION_LINK: undefined,
DISCORD_LINK: undefined,
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_CHROME_EXTENSION_INSTALL_LINK,
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK,
ONBOARDING_PROXY_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_PROXY_INSTALL_LINK,
DMCA_EMAIL: import.meta.env.VITE_DMCA_EMAIL,
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
BACKEND_URL: import.meta.env.VITE_BACKEND_URL,
DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS,
TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY,
CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS,
HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING,
};
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
function getKeyValue(key: keyof Config): string | undefined {
let windowValue = (window as any)?.__CONFIG__?.[`VITE_${key}`];
if (
windowValue !== null &&
windowValue !== undefined &&
windowValue.length === 0
)
windowValue = undefined;
return env[key] ?? windowValue ?? undefined;
}
function getKey(key: keyof Config, defaultString?: string): string {
return getKeyValue(key)?.toString() ?? defaultString ?? "";
}
export function conf(): RuntimeConfig {
const dmcaEmail = getKey("DMCA_EMAIL");
const chromeExtension = getKey("ONBOARDING_CHROME_EXTENSION_INSTALL_LINK");
const firefoxExtension = getKey("ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK");
const proxyInstallLink = getKey("ONBOARDING_PROXY_INSTALL_LINK");
const turnstileKey = getKey("TURNSTILE_KEY");
return {
APP_VERSION,
GITHUB_LINK,
DONATION_LINK,
DISCORD_LINK,
DMCA_EMAIL: dmcaEmail.length > 0 ? dmcaEmail : null,
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK:
chromeExtension.length > 0 ? chromeExtension : null,
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK:
firefoxExtension.length > 0 ? firefoxExtension : null,
ONBOARDING_PROXY_INSTALL_LINK:
proxyInstallLink.length > 0 ? proxyInstallLink : null,
BACKEND_URL: getKey("BACKEND_URL", BACKEND_URL),
TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
PROXY_URLS: getKey("CORS_PROXY_URL")
.split(",")
.map((v) => v.trim()),
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true",
TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null,
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
.split(",")
.map((v) => v.trim())
.filter((v) => v.length > 0), // Should be comma-seperated and contain the media type and ID, formatted like so: movie-753342,movie-753342,movie-753342
CDN_REPLACEMENTS: getKey("CDN_REPLACEMENTS", "")
.split(",")
.map((v) =>
v
.split(":")
.map((s) => s.trim())
.filter((s) => s.length > 0),
)
.filter((v) => v.length === 2), // The format is <beforeA>:<afterA>,<beforeB>:<afterB>
};
}

View file

@ -0,0 +1,6 @@
export const APP_VERSION = import.meta.env.PACKAGE_VERSION;
export const DISCORD_LINK = "https://movie-web.github.io/links/discord";
export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
export const DONATION_LINK = "https://ko-fi.com/movieweb";
export const GA_ID = import.meta.env.VITE_GA_ID;
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;

11
src/utils/setup/ga.ts Normal file
View file

@ -0,0 +1,11 @@
import ReactGA from "react-ga4";
import { GA_ID } from "@/setup/constants";
if (GA_ID) {
ReactGA.initialize([
{
trackingId: GA_ID,
},
]);
}

27
src/utils/setup/i18n.ts Normal file
View file

@ -0,0 +1,27 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { locales } from "@/assets/languages";
import { getLocaleInfo } from "@/utils/language";
// Languages
const langCodes = Object.keys(locales);
const resources = Object.fromEntries(
Object.entries(locales).map((entry) => [entry[0], { translation: entry[1] }]),
);
i18n.use(initReactI18next).init({
fallbackLng: "en",
resources,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export const appLanguageOptions = langCodes.map((lang) => {
const langObj = getLocaleInfo(lang);
if (!langObj)
throw new Error(`Language with code ${lang} cannot be found in database`);
return langObj;
});
export default i18n;

27
src/utils/setup/pwa.ts Normal file
View file

@ -0,0 +1,27 @@
import { registerSW } from "virtual:pwa-register";
const intervalMS = 60 * 60 * 1000;
registerSW({
immediate: true,
onRegisteredSW(swUrl, r) {
if (!r) return;
setInterval(async () => {
if (!(!r.installing && navigator)) return;
if ("connection" in navigator && !navigator.onLine) return;
const resp = await fetch(swUrl, {
cache: "no-store",
headers: {
cache: "no-store",
"cache-control": "no-cache",
},
});
if (resp?.status === 200) {
await r.update();
}
}, intervalMS);
},
});