Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial setup for mentor platform 🧱 #704

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/mentor-platform/.env.example
Original file line number Diff line number Diff line change
@@ -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...


10 changes: 10 additions & 0 deletions apps/mentor-platform/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -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: ['./*', '../*'] }],
},
};
29 changes: 29 additions & 0 deletions apps/mentor-platform/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -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,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
198 changes: 198 additions & 0 deletions apps/mentor-platform/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -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(
<RemixServer context={remixContext} url={request.url} />,
{
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(
<RemixServer context={remixContext} url={request.url} />,
{
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(
`
<html>
<body>
<script>
document.cookie = 'timezone=' + Intl.DateTimeFormat().resolvedOptions().timeZone + '; path=/';
window.location.reload();
</script>
</body>
</html>
`,
{
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);
});
}
91 changes: 91 additions & 0 deletions apps/mentor-platform/app/root.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof loader>();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>

<body>
<Outlet />

{toast && (
<Toast key={toast.id} message={toast.message} type={toast.type} />
)}

<script
// https://remix.run/docs/en/v1/guides/envvars#browser-environment-variables
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>

<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default withSentry(App);
24 changes: 24 additions & 0 deletions apps/mentor-platform/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { MetaFunction } from '@remix-run/node';

export const meta: MetaFunction = () => {
return [
{ title: 'ColorStack Mentor Platform' },
{
name: 'description',
content: 'Welcome to the ColorStack Mentor Platform!',
},
];
};

export default function Index() {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<h1 className="mb-4 text-4xl font-bold">
Welcome to ColorStack Mentor Platform
</h1>
<p className="text-lg text-gray-600">
Connect with mentors and grow your career in tech.
</p>
</div>
);
}
Loading
Loading