diff --git a/.env.example b/.env.example index 9717ca010..890165976 100644 --- a/.env.example +++ b/.env.example @@ -58,13 +58,6 @@ NEXT_PUBLIC_VALIDATOR= NODE_ENV= -########## Umami ########## -## Umami: self-hosted analytics service -## Required only in production. -NEXT_PUBLIC_UMAMI_URL= -NEXT_PUBLIC_UMAMI_WEBSITE_ID= - - ########## Sentry ########## ## Sentry: error tracking service ## Not required in any environments. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e592ec0cf..c5d1ac924 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,7 +80,3 @@ jobs: NODE_ENV: production VERCEL_ENV: preview - - # UMAMI - NEXT_PUBLIC_UMAMI_URL: hi - NEXT_PUBLIC_UMAMI_WEBSITE_ID: bye diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea5cf7edc..09e4346f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,10 +64,6 @@ jobs: NODE_ENV: test - # UMAMI - NEXT_PUBLIC_UMAMI_URL: hi - NEXT_PUBLIC_UMAMI_WEBSITE_ID: bye - steps: - name: Checkout uses: actions/checkout@v3 diff --git a/docs/WORKFLOWS_AND_DEPLOYMENT.md b/docs/WORKFLOWS_AND_DEPLOYMENT.md index 4f760d99d..a05d563ba 100644 --- a/docs/WORKFLOWS_AND_DEPLOYMENT.md +++ b/docs/WORKFLOWS_AND_DEPLOYMENT.md @@ -24,8 +24,6 @@ All deployment/external service: - Web server: Vercel - TRPC Routes: Vercel Edge Functions - Planner Postgres: Neon -- Umami (User analytics): Railway -- Umami Postgres: Railway - Sentry (Crash analytics) - Auth Providers: Discord, Google, Facebook - Mailtrap (Email "magic link" auth) diff --git a/package-lock.json b/package-lock.json index 0d40458d5..3e61693ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^5.4.0", "@next-auth/prisma-adapter": "^1.0.5", "@next/bundle-analyzer": "^13.1.6", + "@next/third-parties": "^15.0.2", "@prisma/client": "^5.0.0", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.3", @@ -2223,6 +2224,18 @@ "node": ">= 10" } }, + "node_modules/@next/third-parties": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-15.0.2.tgz", + "integrity": "sha512-Ohlh0KKfag3Vrx+yuSMJ/fSoCVvRoVG9wRiz8jvYelmg+l0970d41VoGzF2UeKwh9s5qXVRDVqiN/mIeiJ4iLg==", + "dependencies": { + "third-party-capital": "1.0.20" + }, + "peerDependencies": { + "next": "^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^18.2.0 || 19.0.0-rc-02c0e824-20241028" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -15639,6 +15652,11 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/third-party-capital": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz", + "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==" + }, "node_modules/throttleit": { "version": "1.0.0", "dev": true, @@ -18501,6 +18519,14 @@ "integrity": "sha512-Ls2OL9hi3YlJKGNdKv8k3X/lLgc3VmLG3a/DeTkAd+lAituJp8ZHmRmm9f9SL84fT3CotlzcgbdaCDfFwFA6bA==", "optional": true }, + "@next/third-parties": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-15.0.2.tgz", + "integrity": "sha512-Ohlh0KKfag3Vrx+yuSMJ/fSoCVvRoVG9wRiz8jvYelmg+l0970d41VoGzF2UeKwh9s5qXVRDVqiN/mIeiJ4iLg==", + "requires": { + "third-party-capital": "1.0.20" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "requires": { @@ -27268,6 +27294,11 @@ "text-table": { "version": "0.2.0" }, + "third-party-capital": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz", + "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==" + }, "throttleit": { "version": "1.0.0", "dev": true diff --git a/package.json b/package.json index 004b78a8d..365c9b48f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@mui/material": "^5.4.0", "@next-auth/prisma-adapter": "^1.0.5", "@next/bundle-analyzer": "^13.1.6", + "@next/third-parties": "^15.0.2", "@prisma/client": "^5.0.0", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.3", diff --git a/src/components/common/AnalyticsWrapper.tsx b/src/components/common/AnalyticsWrapper.tsx deleted file mode 100644 index 1fc4bc563..000000000 --- a/src/components/common/AnalyticsWrapper.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { cloneElement, Children } from 'react'; - -/** - * Wrapper component that injects ``analyticsClass`` class into its child - * - * Errors when more than 1 child - * - * Child component must take ``className`` props - */ -export default function AnalyticsWrapper({ - analyticsClass, - children, -}: { - analyticsClass: string; - children: React.ReactElement; -}) { - return cloneElement(Children.only(children), { - ...children.props, - className: children.props.className - ? `${children.props.className} ${analyticsClass}` - : analyticsClass, - }); -} diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index bb2e05022..984b4267e 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -7,7 +7,6 @@ import ChevronIcon from '@/icons/ChevronIcon'; import PlusIcon from '@/icons/PlusIcon'; import { trpc } from '@utils/trpc'; -import AnalyticsWrapper from '../common/AnalyticsWrapper'; import PlanCard from '../landing/PlanCard'; import TemplateModal from '../template/Modal'; @@ -122,42 +121,36 @@ export default function PlansPage(): JSX.Element { - - { - setPlanPage(0); - setOpenTemplateModal(true); - }} - /> - + { + setPlanPage(0); + setOpenTemplateModal(true); + }} + /> - - { - setPlanPage(1); - setOpenTemplateModal(true); - }} - /> - + { + setPlanPage(1); + setOpenTemplateModal(true); + }} + /> - - { - setPlanPage(2); - setOpenTemplateModal(true); - }} - /> - + { + setPlanPage(2); + setOpenTemplateModal(true); + }} + /> diff --git a/src/components/planner/Sidebar/Sidebar.tsx b/src/components/planner/Sidebar/Sidebar.tsx index 449080316..00509e231 100644 --- a/src/components/planner/Sidebar/Sidebar.tsx +++ b/src/components/planner/Sidebar/Sidebar.tsx @@ -4,7 +4,6 @@ import Skeleton from 'react-loading-skeleton'; import { v4 as uuidv4 } from 'uuid'; import Button from '@/components/Button'; -import AnalyticsWrapper from '@/components/common/AnalyticsWrapper'; import RequirementsContainer from '@/components/planner/Sidebar/RequirementsContainer'; import SearchBar from '@/components/planner/Sidebar/SearchBar'; import ChevronIcon from '@/icons/ChevronIcon'; @@ -164,21 +163,19 @@ function CourseSelectorContainer({
- - setDisplay(true)} - updateQuery={(q) => { - updateQuery(q); - setDisplay(true); - }} - className={`${ - displayResults - ? 'rounded-b-none border-b-transparent' - : 'rounded-b-[10px] border-b-inherit' - }`} - placeholder="Search courses" - /> - + setDisplay(true)} + updateQuery={(q) => { + updateQuery(q); + setDisplay(true); + }} + className={`${ + displayResults + ? 'rounded-b-none border-b-transparent' + : 'rounded-b-[10px] border-b-inherit' + }`} + placeholder="Search courses" + />
{displaySemesterCode(semester.code)} - - - +
{ > {Object.entries(tagColors).map(([color, classes]) => ( - -
{ - e.stopPropagation(); - toggleColorFilter(color as keyof typeof tagColors); - }} - > - filter.type === 'color' && filter.color === color, - )} - onClick={(e) => e.stopPropagation()} - onCheckedChange={() => toggleColorFilter(color as keyof typeof tagColors)} - /> -
- - {color.substring(0, 1).toUpperCase() + color.substring(1) || 'None'} - -
-
+
{ + e.stopPropagation(); + toggleColorFilter(color as keyof typeof tagColors); + }} + > + filter.type === 'color' && filter.color === color, + )} + onClick={(e) => e.stopPropagation()} + onCheckedChange={() => toggleColorFilter(color as keyof typeof tagColors)} + /> +
+ + {color.substring(0, 1).toUpperCase() + color.substring(1) || 'None'} + +
))} @@ -130,25 +127,23 @@ const FilterByDropdown: FC = ({ children }) => { .sort() .map((year) => ( - -
{ - toggleYearFilter(year); - e.stopPropagation(); - }} - > - e.stopPropagation()} - checked={filters.some( - (filter) => filter.type === 'year' && filter.year === year, - )} - onCheckedChange={() => toggleYearFilter(year)} - /> - {year} -
-
+
{ + toggleYearFilter(year); + e.stopPropagation(); + }} + > + e.stopPropagation()} + checked={filters.some( + (filter) => filter.type === 'year' && filter.year === year, + )} + onCheckedChange={() => toggleYearFilter(year)} + /> + {year} +
))} @@ -180,26 +175,24 @@ const FilterByDropdown: FC = ({ children }) => { > {Object.keys(semestersDisplayMap).map((semesterType) => ( - -
{ - toggleSemesterFilter(semesterType as SemesterType); - e.stopPropagation(); - }} - > - e.stopPropagation()} - checked={filters.some( - (filter) => - filter.type === 'semester' && semesterType === filter.semester, - )} - onCheckedChange={() => toggleSemesterFilter(semesterType as SemesterType)} - /> - {semestersDisplayMap[semesterType as SemesterType] + ' semester'} -
-
+
{ + toggleSemesterFilter(semesterType as SemesterType); + e.stopPropagation(); + }} + > + e.stopPropagation()} + checked={filters.some( + (filter) => + filter.type === 'semester' && semesterType === filter.semester, + )} + onCheckedChange={() => toggleSemesterFilter(semesterType as SemesterType)} + /> + {semestersDisplayMap[semesterType as SemesterType] + ' semester'} +
))} diff --git a/src/components/planner/Toolbar/Toolbar.tsx b/src/components/planner/Toolbar/Toolbar.tsx index 936f6135a..279478e96 100644 --- a/src/components/planner/Toolbar/Toolbar.tsx +++ b/src/components/planner/Toolbar/Toolbar.tsx @@ -3,7 +3,6 @@ import { usePDF } from '@react-pdf/renderer'; import Link from 'next/link'; import { FC, useEffect, useState } from 'react'; -import AnalyticsWrapper from '@/components/common/AnalyticsWrapper'; import DownloadIcon from '@/icons/DownloadIcon'; import SettingsIcon from '@/icons/SettingsIcon'; import SwitchVerticalIcon from '@/icons/SwitchVerticalIcon'; @@ -88,41 +87,37 @@ const Toolbar: FC = ({
- - + - - + Export Degree Plan + + - - - + diff --git a/src/env/schema.mjs b/src/env/schema.mjs index 250ceb214..8db1905b4 100644 --- a/src/env/schema.mjs +++ b/src/env/schema.mjs @@ -40,8 +40,6 @@ export const serverSchema = z.object({ */ export const clientSchema = z.object({ NEXT_PUBLIC_NODE_ENV: z.enum(['development', 'test', 'production']), - NEXT_PUBLIC_UMAMI_URL: z.string(), - NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string(), NEXT_PUBLIC_VALIDATOR: z.string(), }); @@ -53,7 +51,5 @@ export const clientSchema = z.object({ */ export const clientEnv = { NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV, - NEXT_PUBLIC_UMAMI_URL: process.env.NEXT_PUBLIC_UMAMI_URL, - NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, NEXT_PUBLIC_VALIDATOR: process.env.NEXT_PUBLIC_VALIDATOR, }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 2702a2c5a..91679fccb 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,6 +5,7 @@ import '../styles/introjs.css'; import { createTheme, StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { GoogleAnalytics } from '@next/third-parties/google'; import { Analytics } from '@vercel/analytics/react'; import { AnimateSharedLayout } from 'framer-motion'; import { type AppType, AppProps } from 'next/app'; @@ -84,6 +85,7 @@ const NebulaApp: AppType<{ session: Session | null }> = ({ return ( + @@ -105,30 +107,7 @@ const NebulaApp: AppType<{ session: Session | null }> = ({ - - {process.env.VERCEL_ENV === 'production' && ( - - - )} setHasWarned(true)} diff --git a/src/pages/api/umami/[uri].ts b/src/pages/api/umami/[uri].ts deleted file mode 100644 index 7ad7bf244..000000000 --- a/src/pages/api/umami/[uri].ts +++ /dev/null @@ -1,63 +0,0 @@ -import axios from 'axios'; -import { NextApiRequest, NextApiResponse } from 'next'; - -import { env } from '@/env/client.mjs'; - -const scriptName = 'test'; -const endpointName = 'endpoint-name'; -const umamiUrl = env.NEXT_PUBLIC_UMAMI_URL; -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', - 'Access-Control-Max-Age': '86400', -}; - -/*** - * Purpose of this NextJS api endpoint is to allow umami to - * track website usage in spite of adblockers - * - * More information can be found here: - * https://github.com/umami-software/umami/discussions/1026 - * - */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { uri } = req.query; - - if ((uri as string).endsWith(scriptName)) { - return getScript(req, res); - } else if ((uri as string).endsWith(endpointName)) { - return postData(req, res); - } - res.status(404).send(null); -} - -async function getScript(req: NextApiRequest, res: NextApiResponse) { - // Uses axios because I don't feel like changing it rn - // Also host field causes issues when converting to fetch - const response = await axios(umamiUrl + '/umami.js', { - headers: { - ...req.headers, - ...corsHeaders, - 'accept-encoding': 'gzip', - host: null, // not removing host header will result in a weird SSL error that leads to a 500 code (EPROTO SSL alert number 80) - } as unknown as Record, - }); - - const originalScript = await response.data; - const obfuscatedScript = originalScript.replace( - new RegExp('/api/collect', 'g'), - `/${endpointName}`, - ); - res.status(response.status ?? 200).send(obfuscatedScript); -} - -async function postData(req: NextApiRequest, res: NextApiResponse) { - const response = await axios.post(umamiUrl + '/api/collect', req.body, { - headers: { - ...req.headers, - ...corsHeaders, - host: null, // not removing host header will result in a weird SSL error that leads to a 500 code (EPROTO SSL alert number 80) - } as unknown as Record, - }); - res.status(response.status ?? 201).send(response.data); -}