From d9e7a97365fc230bf93f0d28a15ad4fcf47c3b40 Mon Sep 17 00:00:00 2001 From: tas268 Date: Sun, 26 Jan 2025 11:16:31 -0800 Subject: [PATCH 1/2] Add new files to mentor-platform --- apps/mentor-platform/.env.example | 15 ++ apps/mentor-platform/.eslintrc.cjs | 10 + apps/mentor-platform/app/entry.client.tsx | 29 +++ apps/mentor-platform/app/entry.server.tsx | 198 ++++++++++++++++++ apps/mentor-platform/app/root.tsx | 91 ++++++++ apps/mentor-platform/app/routes/_index.tsx | 19 ++ .../app/shared/components/card.tsx | 37 ++++ .../app/shared/constants.server.ts | 42 ++++ apps/mentor-platform/app/shared/constants.ts | 19 ++ .../app/shared/cookies.server.ts | 23 ++ .../app/shared/hooks/use-mixpanel-tracker.tsx | 28 +++ .../app/shared/hooks/use-toast.tsx | 23 ++ .../app/shared/session.server.ts | 76 +++++++ apps/mentor-platform/app/tailwind.css | 18 ++ apps/mentor-platform/env.d.ts | 2 + apps/mentor-platform/global.d.ts | 12 ++ apps/mentor-platform/package.json | 46 ++++ apps/mentor-platform/postcss.config.mjs | 5 + .../public/apple-touch-icon.png | Bin 0 -> 1865 bytes apps/mentor-platform/public/favicon.ico | Bin 0 -> 15406 bytes .../public/images/colorstack-background.png | Bin 0 -> 5146 bytes .../public/images/colorstack-wordmark.png | Bin 0 -> 143322 bytes .../public/images/linkedin.png | Bin 0 -> 5823 bytes apps/mentor-platform/railway.json | 22 ++ apps/mentor-platform/tailwind.config.js | 11 + apps/mentor-platform/tsconfig.json | 12 ++ apps/mentor-platform/vite.config.ts | 11 + packages/core/package.json | 2 + packages/core/src/mentor-platform.server.ts | 0 packages/core/src/mentor-platform.ui.ts | 1 + 30 files changed, 752 insertions(+) create mode 100644 apps/mentor-platform/.env.example create mode 100644 apps/mentor-platform/.eslintrc.cjs create mode 100644 apps/mentor-platform/app/entry.client.tsx create mode 100644 apps/mentor-platform/app/entry.server.tsx create mode 100644 apps/mentor-platform/app/root.tsx create mode 100644 apps/mentor-platform/app/routes/_index.tsx create mode 100644 apps/mentor-platform/app/shared/components/card.tsx create mode 100644 apps/mentor-platform/app/shared/constants.server.ts create mode 100644 apps/mentor-platform/app/shared/constants.ts create mode 100644 apps/mentor-platform/app/shared/cookies.server.ts create mode 100644 apps/mentor-platform/app/shared/hooks/use-mixpanel-tracker.tsx create mode 100644 apps/mentor-platform/app/shared/hooks/use-toast.tsx create mode 100644 apps/mentor-platform/app/shared/session.server.ts create mode 100644 apps/mentor-platform/app/tailwind.css create mode 100644 apps/mentor-platform/env.d.ts create mode 100644 apps/mentor-platform/global.d.ts create mode 100644 apps/mentor-platform/package.json create mode 100644 apps/mentor-platform/postcss.config.mjs create mode 100644 apps/mentor-platform/public/apple-touch-icon.png create mode 100644 apps/mentor-platform/public/favicon.ico create mode 100644 apps/mentor-platform/public/images/colorstack-background.png create mode 100644 apps/mentor-platform/public/images/colorstack-wordmark.png create mode 100644 apps/mentor-platform/public/images/linkedin.png create mode 100644 apps/mentor-platform/railway.json create mode 100644 apps/mentor-platform/tailwind.config.js create mode 100644 apps/mentor-platform/tsconfig.json create mode 100644 apps/mentor-platform/vite.config.ts create mode 100644 packages/core/src/mentor-platform.server.ts create mode 100644 packages/core/src/mentor-platform.ui.ts diff --git a/apps/mentor-platform/.env.example b/apps/mentor-platform/.env.example new file mode 100644 index 000000000..a1cd71bb6 --- /dev/null +++ b/apps/mentor-platform/.env.example @@ -0,0 +1,15 @@ +# Required to run the Mentor Platform in development... + +ADMIN_DASHBOARD_URL=http://localhost:3001 +API_URL=http://localhost:8080 +DATABASE_URL=postgresql://oyster:oyster@localhost:5433/oyster +ENVIRONMENT=development +JWT_SECRET=_ +MEMBER_PROFILE_URL=http://localhost:3000 +MENTOR_PLATFORM_URL=http://localhost:3002 +REDIS_URL=redis://localhost:6380 +SESSION_SECRET=_ + +# Optional for development, but won't be able to run certain features... + + diff --git a/apps/mentor-platform/.eslintrc.cjs b/apps/mentor-platform/.eslintrc.cjs new file mode 100644 index 000000000..13d694096 --- /dev/null +++ b/apps/mentor-platform/.eslintrc.cjs @@ -0,0 +1,10 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ['@oyster/eslint-config/base'], + parserOptions: { + tsconfigRootDir: __dirname, + }, + rules: { + 'no-restricted-imports': ['error', { patterns: ['./*', '../*'] }], + }, +}; diff --git a/apps/mentor-platform/app/entry.client.tsx b/apps/mentor-platform/app/entry.client.tsx new file mode 100644 index 000000000..78c992e27 --- /dev/null +++ b/apps/mentor-platform/app/entry.client.tsx @@ -0,0 +1,29 @@ +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { startTransition, StrictMode, useEffect } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +Sentry.init({ + dsn: window.env.SENTRY_DSN, + enabled: window.env.ENVIRONMENT !== 'development', + environment: window.env.ENVIRONMENT, + tracesSampleRate: 0.25, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.remixRouterInstrumentation( + useEffect, + useLocation, + useMatches + ), + }), + ], +}); + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/apps/mentor-platform/app/entry.server.tsx b/apps/mentor-platform/app/entry.server.tsx new file mode 100644 index 000000000..0456bfe8e --- /dev/null +++ b/apps/mentor-platform/app/entry.server.tsx @@ -0,0 +1,198 @@ +import type { EntryContext } from '@remix-run/node'; +import { createReadableStreamFromReadable } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import dayjs from 'dayjs'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import timezone from 'dayjs/plugin/timezone.js'; +import updateLocale from 'dayjs/plugin/updateLocale'; +import utc from 'dayjs/plugin/utc.js'; +import isbot from 'isbot'; +import { renderToPipeableStream } from 'react-dom/server'; +import { PassThrough } from 'stream'; + +import { getCookie, run } from '@oyster/utils'; + +import { ENV } from '@/shared/constants.server'; + +// Importing this file ensures that our application has all of the environment +// variables necessary to run. If any are missing, this file will throw an error +// and crash the application. +import '@/shared/constants.server'; + +run(() => { + dayjs.extend(utc); + dayjs.extend(relativeTime); + dayjs.extend(timezone); + dayjs.extend(advancedFormat); + dayjs.extend(updateLocale); + + // To use relative times in Day.js, we need to extend some of the above plugins, + // and now we'll update the format of the relative time to be more concise. + // https://day.js.org/docs/en/customization/relative-time + dayjs.updateLocale('en', { + relativeTime: { + past: '%s', + s: '%ds', + m: '1m', + mm: '%dm', + h: '1h', + hh: '%dh', + d: '1d', + dd: '%dd', + M: '1mo', + MM: '%dmo', + y: '1y', + yy: '%dy', + }, + }); +}); + +Sentry.init({ + dsn: ENV.SENTRY_DSN, + enabled: ENV.ENVIRONMENT !== 'development', + environment: ENV.ENVIRONMENT, + tracesSampleRate: 0.5, +}); + +const ABORT_DELAY = 5000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const bot: boolean = isbot(request.headers.get('user-agent')); + + return bot + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let didError = false; + + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady: () => { + const body = new PassThrough(); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(createReadableStreamFromReadable(body), { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError: (error: unknown) => { + reject(error); + }, + onError: (error: unknown) => { + didError = true; + + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let didError = false; + + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady: () => { + const cookie = request.headers.get('Cookie'); + + const timezone = getCookie(cookie || '', 'timezone'); + + // @see https://www.jacobparis.com/content/remix-ssr-dates + // In order to match the timezone of dates on both the client and + // the server, we need to get the timezone from the client. How we're + // doing this: if that timezone cookie value isn't present, then we're + // setting a cookie with the timezone on the client and reloading the + // page. + if (!timezone) { + return resolve( + new Response( + ` + + + + + + `, + { + headers: { + 'Content-Type': 'text/html', + 'Set-Cookie': 'timezone=America/New_York; path=/', + Refresh: `0; url=${request.url}`, + }, + } + ) + ); + } + + const body = new PassThrough(); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(createReadableStreamFromReadable(body), { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError: (error: unknown) => { + reject(error); + }, + onError: (error: unknown) => { + didError = true; + + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/apps/mentor-platform/app/root.tsx b/apps/mentor-platform/app/root.tsx new file mode 100644 index 000000000..8b6db9e37 --- /dev/null +++ b/apps/mentor-platform/app/root.tsx @@ -0,0 +1,91 @@ +import type { LinksFunction, MetaFunction } from '@remix-run/node'; +import { json, type LoaderFunctionArgs } from '@remix-run/node'; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, +} from '@remix-run/react'; +import { withSentry } from '@sentry/remix'; + +import { buildMeta } from '@oyster/core/remix'; +import { Toast } from '@oyster/ui'; +import uiStylesheet from '@oyster/ui/index.css?url'; + +import { ENV } from '@/shared/constants.server'; +import { commitSession, getSession, SESSION } from '@/shared/session.server'; +import tailwindStylesheet from '@/tailwind.css?url'; + +export const links: LinksFunction = () => { + return [ + { rel: 'stylesheet', href: uiStylesheet }, + { rel: 'stylesheet', href: tailwindStylesheet }, + ]; +}; + +export const meta: MetaFunction = () => { + return buildMeta({ + description: `Your home for ColorStack mentoring.`, + title: 'Mentor Platform', + }); +}; + +export async function loader({ request }: LoaderFunctionArgs) { + const session = await getSession(request); + + const toast = session.get(SESSION.TOAST); + + const env: Window['env'] = { + ENVIRONMENT: ENV.ENVIRONMENT, + SENTRY_DSN: ENV.SENTRY_DSN, + }; + + return json( + { + env, + toast: toast || null, + }, + { + headers: { + 'Set-Cookie': await commitSession(session), + }, + } + ); +} + +function App() { + const { env, toast } = useLoaderData(); + + return ( + + + + + + + + + + + + {toast && ( + + )} + +