diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..fbce0b866 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['colorstack'], + root: true, +}; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..173dd969c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ramiAbdou \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..6de00d586 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug Report 🐞 +about: Report any issues with the platform. +title: '' +labels: bug +assignees: '' +--- + +### Description + +A clear and concise description of what the bug is. + +### Steps to Reproduce + +Steps to reproduce the behavior: + +1. Go to '...'. +2. Click on '...'. +3. Scroll down to '...'. +4. See error. + +### Expected Behavior + +A clear and concise description of what you expected to happen. + +### Screenshots + +If applicable, add screenshots to help explain your problem. + +### Additional Context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..589b32772 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Ask a Question ❓ + url: https://colorstack-family.slack.com + about: Ask a general question about the codebase in our Slack community. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..1d795a967 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request 🙏 +about: Suggest a feature to have in ColorStack. +title: '' +labels: feature +assignees: '' +--- + +### Description + +A clear and concise description of what the feature should be. + +### Problem / Value Proposition + +What problem does this solve? What value does it add to ColorStack? + +### Additional Context + +Add any other context about the problem here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..3f0282aa3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +## Description ✏️ + +Closes #xxx + +Describe what this PR does. + +- Bullet points are helpful. +- Screenshots are helpful, if applicable. + +## Type of Change 🐞 + +- [ ] Feature - A non-breaking change which adds functionality. +- [ ] Fix - A non-breaking change which fixes an issue. +- [ ] Refactor - A change that neither fixes a bug nor adds a feature. +- [ ] Documentation - A change only to in-code or markdown documentation. +- [ ] Tests - A change that adds missing unit/integration tests. +- [ ] Chore - A change that is likely none of the above. + +## Checklist ✅ + +- [ ] I have done a self-review of my code. +- [ ] I have manually tested my code (if applicable). +- [ ] I have added/updated any relevant documentation (if applicable). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..003538707 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: Continuous Integration + +on: + pull_request: + types: [opened, synchronize] + +jobs: + build: + name: Lint, Build and Test + runs-on: ubuntu-latest + timeout-minutes: 15 + + services: + postgres: + image: postgres + env: + POSTGRES_DB: colorstack + POSTGRES_PASSWORD: password + POSTGRES_USER: username + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready --health-interval 10s --health-timeout 5s + --health-retries 5 + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" --health-interval 10s --health-timeout + 5s --health-retries 5 + + env: + DATABASE_URL: postgres://username:password@localhost:5432/colorstack + REDIS_URL: redis://localhost:6379 + + steps: + - name: Checkout repository code + uses: actions/checkout@v4 + + - name: Cache Turborepo setup + uses: actions/cache@v4 + with: + # When running turbo commands, we use `.turbo` as the cache directory. + # See the root `package.json` for reference. + path: .turbo + + key: ${{ runner.os }}-turbo-${{ github.sha }} + + # This is how GitHub Actions restores the cache. Because we only + # the prefix of the key (and not the `github.sha`), this will allow + # us to hit the cache on any CI runs on commit on any branch. + restore-keys: | + ${{ runner.os }}-turbo- + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run database migrations and generate types + run: yarn db:migrate + + - name: Lint + run: yarn lint + + - name: Build all packages and applications + run: yarn build + + - name: Run tests + run: yarn test diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..814030a8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies + +node_modules + +# Environment Variables + +.env + +# Turborepo + +.turbo + +# React Email + +.react-email + +# Remix Run + +.cache + +# Build + +build +dist +tsconfig.tsbuildinfo + + +# Debugging + +yarn-debug.log* +yarn-error.log* + + +# Miscelleanous + +.DS_Store diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..9118330f0 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +module.exports = { + printWidth: 80, + proseWrap: 'always', + semi: true, + singleQuote: true, + trailingComma: 'es5', + tailwindFunctions: ['cx'], +}; diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..1c3864341 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [80], + "editor.quickSuggestions": { "strings": "on" }, + "eslint.workingDirectories": ["./apps/*", "./config/*", "./packages/*"], + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.tsdk": "node_modules/typescript/lib", + "[typescript][typescriptreact]": { + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + }, + "tailwindCSS.experimental.classRegex": [ + ["cx\\(([^)]+)\\)", "'([^']*)'"], + ["cx\\(([^)]+)\\)", "`([^`]*)`"] + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..bd60b426c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people. +- Being respectful of differing opinions, viewpoints, and experiences. +- Giving and gracefully accepting constructive feedback. +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience. +- Focusing on what is best not just for us as individuals, but for the overall + community. + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind. +- Trolling, insulting or derogatory comments, and personal or political attacks. +- Public or private harassment. +- Publishing others' private information, such as a physical or email address, + without their explicit permission. +- Other conduct which could reasonably be considered inappropriate in a + professional setting. + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +rami@colorstack.org. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1f8b66d64 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,195 @@ +# Contributing + +First off, thank you for taking the time to contribute! 🥳 ColorStack is nothing +without its community, and that certainly extends to the software that we build. +This is a big team effort! + +## Deciding What to Work On + +You can start by browsing through our list of +[issues](https://github.com/colorstackorg/colorstack/issues) or creating your +own issue that would improve our product. Once you've decided on an issue, leave +a comment and wait to get approval from one of our codebase admins - this helps +avoid multiple people working on this same issue. + +## Making a Pull Request + +Some things to keep in mind when making a pull request: + +- The target branch in our repository is `main`. +- Fill out the PR template accordingly. +- The name of the PR should: + - Start with one of the following prefixes: + - `feat`: A non-breaking change which adds functionality. + - `fix`: A non-breaking change which fixes an issue. + - `refactor`: A change that neither fixes a bug nor adds a feature. + - `docs`: A change only to in-code or markdown documentation. + - `test`: A change that adds missing tests. + - `chore`: A change that is likely none of the above. + - Be present tense (ie: "Fix", not "Fixed"). + - Start with a verb (ie: "Fix ....", "Add ...", "Implement ..."). + - Have an emoji at the end of it (we like color around here). 🔥 +- Each PR should be attached to an issue, so be sure to add this to the PR + description: + ``` + Closes #. + ``` + See more about + [linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). +- A PR can only be merged (by a maintainer) if: + - A maintainer has reviewed and approved it. + - All CI checks have passed. See [this](./.github/workflows/ci.yml) workflow + for more details. + - All branches are up to date before merging. + - All conversations are resolved. + +## Local Development + +To get started with local development, please follow these simple steps. + +### Prerequisites + +Please ensure that you have the following software on your machine: + +- [Node.js](https://nodejs.org/en/download/package-manager) (v20.x) +- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) (v1) +- [PostgreSQL](https://www.postgresql.org/download/) (v15.x) +- [Redis](https://redis.io/docs/install/install-redis/) + +### Fork and Clone Repository + +1. [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) + the repository to your own GitHub account. +2. [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) + the repository to your local machine. + ``` + git clone https://github.com//colorstack.git + ``` +3. Create a new branch. + ``` + git checkout -b YOUR_BRANCH_NAME + ``` +4. Install all project dependencies. + + ```sh + yarn + ``` + +### Environment Variables + +Set up your environment variable files by duplicating `.env.example` to `.env` +in a few places: + +- `/apps/admin-dashboard/.env.example` +- `/apps/api/.env.example` +- `/apps/member-profile/.env.example` +- `/packages/core/.env.example` + +You'll notice that a lot of environment variables are empty. Most of these empty +variables are tied to the 3rd party integrations we have with platforms such as +Postmark for sending emails and Google for authentication. If you would like to +enable these integrations in development, please see the +[How to Enable Integrations](./docs/how-to-enable-integrations.md) +documentation. + +### Database Setup + +You'll need to make sure that Postgres and Redis are running in the background. + +#### Postgres Setup + +Once Postgres is running, you can connect to it by running: + +```sh +psql +``` + +You'll then need to create 2 Postgres databases locally named `colorstack` and +`colorstack-test`. Within the `psql` terminal, run: + +```postgresql +CREATE DATABASE colorstack; +CREATE DATABASE colorstack-test; +``` + +You should now be able to connect to each database like this: + +```sh +psql colorstack +``` + +#### Executing Database Migrations + +To execute the database migrations, run: + +```sh +yarn db:migrate +``` + +To verify that the migration was executed successfully, connect to your Postgres +database and run: + +```sh +\d +``` + +You should see a bunch of SQL tables! + +#### Seeding the Database + +Now that we have some tables, we're ready to add some seed data in our database, +which will enable you to log into the Admin Dashboard and Member Profile. Run: + +```sh +yarn workspace @colorstack/core db:seed +``` + +Follow the prompt to add your email, and you will now be able to log into both +applications. + +### Building the Project + +You can build the project by running: + +```sh +yarn build +``` + +### Running the Applications + +To run all of our _packages and applications_, you can run: + +```sh +yarn dev +``` + +To run all of our _applications_, you can run: + +```sh +yarn dev:apps +``` + +To run a _specific package or application_, you can use the `--filter` flag like +this: + +```sh +yarn dev --filter=api +``` + +### Editor Setup + +Surprise, surprise. We use [VSCode](https://code.visualstudio.com/download) to +write code! After you download it, we'll need to enable some extensions to make +life a bit easier: + +- [Auto Rename Tag](https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag) +- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) +- [Live Share](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare) +- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +- [Tailwind IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) +- [Typescript Importer](https://marketplace.visualstudio.com/items?itemName=pmneo.tsimporter) + +## License + +By contributing your code to the this GitHub repository, you agree to license +your contribution under the MIT license. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..98f339aeb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2024 ColorStack + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index e845566c0..3bdfca89d 100644 --- a/README.md +++ b/README.md @@ -1 +1,108 @@ -README +

+ +

+ +

The open-source software that powers the ColorStack community experience. ✊🏿✊🏾✊🏽✊🏼

+ +

+ Website | + Family Application +

+ +Member Profile + +## Applications + +- [**Member Profile**](./apps/member-profile/package.json) - Serves as the home + for a ColorStack member, allowing them to manage their information, find and + connect with other members, as well as events and gamification. +- [**Admin Dashboard**](./apps/admin-dashboard/package.json) - An internal + dashboard that the ColorStack team uses to manage important workflows like + application review. +- [**API**](./apps/api/package.json) - Handles all background jobs as well as + any webhook integrations that we have with external services. + +## Understanding the Codebase + +### Important Tools & Technologies + +Here is a list of important tools and technologies that power the ColorStack +codebase: + +- [BullMQ](https://docs.bullmq.io) to queue and process jobs asynchronously (in + the background). +- [Kysely](https://kysely.dev) to query our SQL database in a type-safe way. +- [Node.js](https://nodejs.org/en/about)...because yes! +- [PostgreSQL](https://www.postgresql.org/docs/15/index.html) to store all of + our data. +- [Railway](https://railway.app) to host our applications and databases. +- [Redis](https://redis.io) to store simple key/value based data. +- [Remix](https://remix.run)/[React](https://react.dev) to build fast, + accessible and delightful UI experiences. +- [Tailwind](https://tailwindcss.com) because scaling CSS is hard. +- [Turborepo](https://turbo.build/repo) to manage the build system for our + Typescript monorepo. +- [Typescript](https://www.typescriptlang.org) because we like Javascript...and + we like type checking. +- [Zod](https://zod.dev) to validate all of our data and automatically generate + types. + +### Folder Structure + +Here is the structure of our Typescript monorepo: + +``` +apps +|--- admin-dashboard +|--- api +|--- member-profile +packages +|--- core +|--- core-ui +|--- email-templates +|--- feature-ui +|--- types +|--- utils +``` + +The `apps` directory houses all of our applications (see "Applications" +section). + +The `packages` directory contains reusable pieces of code that are used across +our applications. + +- `core`: Nearly all of our business logic, including our database layer and + more. Will eventually colocate feature-based UI next to its related business + logic. +- `core-ui`: Reusable UI components built in React. +- `email-templates`: React-based email templates built with + [Resend](https://resend.com). +- `feature-ui`: Now deprecated, but previously contained more complex UI code. +- `types`: Miscellaneous types shared across applications. +- `utils`: Reusable utility functions, such as `sleep`. + +### CI Pipeline (GitHub Actions) + +To ensure that we don't have any breaking changes, we have a GitHub Actions +workflow that runs, which can block a PR from being merged if certain checks +don't pass. + +For more information on how that CI pipeline works, see +[this](./.github/workflows/ci.yml) file. + +### Deployment + +We use [Railway](https://railway.app) to host our applications as well as our +PostgreSQL and Redis databases. Whenever we make some changes to our `main` +branch, Railway will automatically pick up those changes and deploy a new +version of our applications. + +Each application has a `railway.json` file where we can configure certain +settings and instructions so Railway knows _how and when_ to build/start our +application. See [this](./apps/api/railway.json) file as an example. For a full +list on what we can configure, see +[here](https://docs.railway.app/reference/config-as-code#configurable-settings). + +## Contributing + +Please see our [contributing guide](./CONTRIBUTING.md)! 👋 diff --git a/apps/admin-dashboard/.env.example b/apps/admin-dashboard/.env.example new file mode 100644 index 000000000..0bdadeff6 --- /dev/null +++ b/apps/admin-dashboard/.env.example @@ -0,0 +1,14 @@ +# Required to run the Admin Dashboard in development... + +ADMIN_DASHBOARD_URL=http://localhost:3001 +API_URL=http://localhost:8080 +DATABASE_URL=postgresql://localhost:5432/colorstack +ENVIRONMENT=development +JWT_SECRET=_ +REDIS_URL=redis://localhost:6379 +SESSION_SECRET=_ + +# Optional for development, but won't be able to run certain features... + +# GOOGLE_CLIENT_ID= +# SENTRY_DSN= \ No newline at end of file diff --git a/apps/admin-dashboard/.eslintrc.js b/apps/admin-dashboard/.eslintrc.js new file mode 100644 index 000000000..379766a7e --- /dev/null +++ b/apps/admin-dashboard/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['colorstack'], +}; diff --git a/apps/admin-dashboard/app/entry.client.tsx b/apps/admin-dashboard/app/entry.client.tsx new file mode 100644 index 000000000..1f635d147 --- /dev/null +++ b/apps/admin-dashboard/app/entry.client.tsx @@ -0,0 +1,29 @@ +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { StrictMode, startTransition, 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/admin-dashboard/app/entry.server.tsx b/apps/admin-dashboard/app/entry.server.tsx new file mode 100644 index 000000000..1a4cdc1ab --- /dev/null +++ b/apps/admin-dashboard/app/entry.server.tsx @@ -0,0 +1,168 @@ +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 timezone from 'dayjs/plugin/timezone.js'; +import utc from 'dayjs/plugin/utc.js'; +import isbot from 'isbot'; +import { renderToPipeableStream } from 'react-dom/server'; +import { PassThrough } from 'stream'; + +import { getCookie } from '@colorstack/utils'; + +// 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 { ENV } from './shared/constants.server'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +Sentry.init({ + dsn: ENV.SENTRY_DSN, + enabled: ENV.ENVIRONMENT !== 'development', + environment: ENV.ENVIRONMENT, + tracesSampleRate: 0.25, +}); + +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/admin-dashboard/app/root.tsx b/apps/admin-dashboard/app/root.tsx new file mode 100644 index 000000000..7ea236fc6 --- /dev/null +++ b/apps/admin-dashboard/app/root.tsx @@ -0,0 +1,85 @@ +import type { LinksFunction, MetaFunction } from '@remix-run/node'; +import { json, LoaderFunctionArgs } from '@remix-run/node'; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, +} from '@remix-run/react'; +import { withSentry } from '@sentry/remix'; + +import { Toast } from '@colorstack/core-ui'; + +import coreUiStylesheet from '@colorstack/core-ui/dist/index.css?url'; +import tailwindStylesheet from './tailwind.css?url'; + +import { ENV } from './shared/constants.server'; +import { commitSession, getSession, SESSION } from './shared/session.server'; + +export const links: LinksFunction = () => { + return [ + { rel: 'stylesheet', href: coreUiStylesheet }, + { rel: 'stylesheet', href: tailwindStylesheet }, + ]; +}; + +export const meta: MetaFunction = () => { + return [{ title: 'ColorStack | Admin Dashboard' }]; +}; + +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 && } + + + + + `, + { + 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/member-profile/app/root.tsx b/apps/member-profile/app/root.tsx new file mode 100644 index 000000000..0f9e29b29 --- /dev/null +++ b/apps/member-profile/app/root.tsx @@ -0,0 +1,91 @@ +import type { LinksFunction, MetaFunction } from '@remix-run/node'; +import { json, LoaderFunctionArgs } from '@remix-run/node'; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, +} from '@remix-run/react'; +import { withSentry } from '@sentry/remix'; +import dayjs from 'dayjs'; +import timezone from 'dayjs/plugin/timezone.js'; +import utc from 'dayjs/plugin/utc.js'; + +import { Toast } from '@colorstack/core-ui'; + +import coreUiStylesheet from '@colorstack/core-ui/dist/index.css?url'; +import tailwindStylesheet from './tailwind.css?url'; + +import { ENV } from './shared/constants.server'; +import { commitSession, getSession, SESSION } from './shared/session.server'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export const links: LinksFunction = () => { + return [ + { rel: 'stylesheet', href: coreUiStylesheet }, + { rel: 'stylesheet', href: tailwindStylesheet }, + ]; +}; + +export const meta: MetaFunction = () => { + return [{ title: 'ColorStack | Member Profile' }]; +}; + +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 && } + +